src.elv.sh@v0.21.0-dev.0.20240515223629-06979efb9a2a/pkg/cli/tk/codearea_test.go (about) 1 package tk 2 3 import ( 4 "reflect" 5 "testing" 6 7 "src.elv.sh/pkg/cli/term" 8 "src.elv.sh/pkg/tt" 9 "src.elv.sh/pkg/ui" 10 ) 11 12 var Args = tt.Args 13 14 var bb = term.NewBufferBuilder 15 16 func p(t ui.Text) func() ui.Text { return func() ui.Text { return t } } 17 18 var codeAreaRenderTests = []renderTest{ 19 { 20 Name: "prompt only", 21 Given: NewCodeArea(CodeAreaSpec{ 22 Prompt: p(ui.T("~>", ui.Bold))}), 23 Width: 10, Height: 24, 24 Want: bb(10).WriteStringSGR("~>", "1").SetDotHere(), 25 }, 26 { 27 Name: "rprompt only", 28 Given: NewCodeArea(CodeAreaSpec{ 29 RPrompt: p(ui.T("RP", ui.Inverse))}), 30 Width: 10, Height: 24, 31 Want: bb(10).SetDotHere().WriteSpaces(8).WriteStringSGR("RP", "7"), 32 }, 33 { 34 Name: "code only with dot at beginning", 35 Given: NewCodeArea(CodeAreaSpec{State: CodeAreaState{ 36 Buffer: CodeBuffer{Content: "code", Dot: 0}}}), 37 Width: 10, Height: 24, 38 Want: bb(10).SetDotHere().Write("code"), 39 }, 40 { 41 Name: "code only with dot at middle", 42 Given: NewCodeArea(CodeAreaSpec{State: CodeAreaState{ 43 Buffer: CodeBuffer{Content: "code", Dot: 2}}}), 44 Width: 10, Height: 24, 45 Want: bb(10).Write("co").SetDotHere().Write("de"), 46 }, 47 { 48 Name: "code only with dot at end", 49 Given: NewCodeArea(CodeAreaSpec{State: CodeAreaState{ 50 Buffer: CodeBuffer{Content: "code", Dot: 4}}}), 51 Width: 10, Height: 24, 52 Want: bb(10).Write("code").SetDotHere(), 53 }, 54 { 55 Name: "prompt, code and rprompt", 56 Given: NewCodeArea(CodeAreaSpec{ 57 Prompt: p(ui.T("~>")), 58 RPrompt: p(ui.T("RP")), 59 State: CodeAreaState{Buffer: CodeBuffer{Content: "code", Dot: 4}}}), 60 Width: 10, Height: 24, 61 Want: bb(10).Write("~>code").SetDotHere().Write(" RP"), 62 }, 63 64 { 65 Name: "prompt explicitly hidden ", 66 Given: NewCodeArea(CodeAreaSpec{ 67 Prompt: p(ui.T("~>")), 68 RPrompt: p(ui.T("RP")), 69 State: CodeAreaState{Buffer: CodeBuffer{Content: "code", Dot: 4}, HideRPrompt: true}}), 70 Width: 10, Height: 24, 71 Want: bb(10).Write("~>code").SetDotHere(), 72 }, 73 { 74 Name: "rprompt too long", 75 Given: NewCodeArea(CodeAreaSpec{ 76 Prompt: p(ui.T("~>")), 77 RPrompt: p(ui.T("1234")), 78 State: CodeAreaState{Buffer: CodeBuffer{Content: "code", Dot: 4}}}), 79 Width: 10, Height: 24, 80 Want: bb(10).Write("~>code").SetDotHere(), 81 }, 82 { 83 Name: "highlighted code", 84 Given: NewCodeArea(CodeAreaSpec{ 85 Highlighter: func(code string) (ui.Text, []ui.Text) { 86 return ui.T(code, ui.Bold), nil 87 }, 88 State: CodeAreaState{Buffer: CodeBuffer{Content: "code", Dot: 4}}}), 89 Width: 10, Height: 24, 90 Want: bb(10).WriteStringSGR("code", "1").SetDotHere(), 91 }, 92 { 93 Name: "tips", 94 Given: NewCodeArea(CodeAreaSpec{ 95 Prompt: p(ui.T("> ")), 96 Highlighter: func(code string) (ui.Text, []ui.Text) { 97 return ui.T(code), []ui.Text{ui.T("static error")} 98 }, 99 State: CodeAreaState{Buffer: CodeBuffer{Content: "code", Dot: 4}}}), 100 Width: 10, Height: 24, 101 Want: bb(10).Write("> code").SetDotHere(). 102 Newline().Write("static error"), 103 }, 104 { 105 Name: "hiding tips", 106 Given: NewCodeArea(CodeAreaSpec{ 107 Prompt: p(ui.T("> ")), 108 Highlighter: func(code string) (ui.Text, []ui.Text) { 109 return ui.T(code), []ui.Text{ui.T("static error")} 110 }, 111 State: CodeAreaState{ 112 Buffer: CodeBuffer{Content: "code", Dot: 4}, HideTips: true}}), 113 Width: 10, Height: 24, 114 Want: bb(10).Write("> code").SetDotHere(), 115 }, 116 { 117 Name: "pending code inserting at the dot", 118 Given: NewCodeArea(CodeAreaSpec{State: CodeAreaState{ 119 Buffer: CodeBuffer{Content: "code", Dot: 4}, 120 Pending: PendingCode{From: 4, To: 4, Content: "x"}, 121 }}), 122 Width: 10, Height: 24, 123 Want: bb(10).Write("code").WriteStringSGR("x", "4").SetDotHere(), 124 }, 125 { 126 Name: "pending code replacing at the dot", 127 Given: NewCodeArea(CodeAreaSpec{State: CodeAreaState{ 128 Buffer: CodeBuffer{Content: "code", Dot: 2}, 129 Pending: PendingCode{From: 2, To: 4, Content: "x"}, 130 }}), 131 Width: 10, Height: 24, 132 Want: bb(10).Write("co").WriteStringSGR("x", "4").SetDotHere(), 133 }, 134 { 135 Name: "pending code to the left of the dot", 136 Given: NewCodeArea(CodeAreaSpec{State: CodeAreaState{ 137 Buffer: CodeBuffer{Content: "code", Dot: 4}, 138 Pending: PendingCode{From: 1, To: 3, Content: "x"}, 139 }}), 140 Width: 10, Height: 24, 141 Want: bb(10).Write("c").WriteStringSGR("x", "4").Write("e").SetDotHere(), 142 }, 143 { 144 Name: "pending code to the right of the cursor", 145 Given: NewCodeArea(CodeAreaSpec{State: CodeAreaState{ 146 Buffer: CodeBuffer{Content: "code", Dot: 1}, 147 Pending: PendingCode{From: 2, To: 3, Content: "x"}, 148 }}), 149 Width: 10, Height: 24, 150 Want: bb(10).Write("c").SetDotHere().Write("o"). 151 WriteStringSGR("x", "4").Write("e"), 152 }, 153 { 154 Name: "ignore invalid pending code 1", 155 Given: NewCodeArea(CodeAreaSpec{State: CodeAreaState{ 156 Buffer: CodeBuffer{Content: "code", Dot: 4}, 157 Pending: PendingCode{From: 2, To: 1, Content: "x"}, 158 }}), 159 Width: 10, Height: 24, 160 Want: bb(10).Write("code").SetDotHere(), 161 }, 162 { 163 Name: "ignore invalid pending code 2", 164 Given: NewCodeArea(CodeAreaSpec{State: CodeAreaState{ 165 Buffer: CodeBuffer{Content: "code", Dot: 4}, 166 Pending: PendingCode{From: 5, To: 6, Content: "x"}, 167 }}), 168 Width: 10, Height: 24, 169 Want: bb(10).Write("code").SetDotHere(), 170 }, 171 { 172 Name: "prioritize lines before the cursor with small height", 173 Given: NewCodeArea(CodeAreaSpec{State: CodeAreaState{ 174 Buffer: CodeBuffer{Content: "a\nb\nc\nd", Dot: 3}, 175 }}), 176 Width: 10, Height: 2, 177 Want: bb(10).Write("a").Newline().Write("b").SetDotHere(), 178 }, 179 { 180 Name: "show only the cursor line when height is 1", 181 Given: NewCodeArea(CodeAreaSpec{State: CodeAreaState{ 182 Buffer: CodeBuffer{Content: "a\nb\nc\nd", Dot: 3}, 183 }}), 184 Width: 10, Height: 1, 185 Want: bb(10).Write("b").SetDotHere(), 186 }, 187 { 188 Name: "show lines after the cursor when all lines before the cursor are shown", 189 Given: NewCodeArea(CodeAreaSpec{State: CodeAreaState{ 190 Buffer: CodeBuffer{Content: "a\nb\nc\nd", Dot: 3}, 191 }}), 192 Width: 10, Height: 3, 193 Want: bb(10).Write("a").Newline().Write("b").SetDotHere(). 194 Newline().Write("c"), 195 }, 196 } 197 198 func TestCodeArea_Render(t *testing.T) { 199 testRender(t, codeAreaRenderTests) 200 } 201 202 var codeAreaHandleTests = []handleTest{ 203 { 204 Name: "simple inserts", 205 Given: NewCodeArea(CodeAreaSpec{}), 206 Events: []term.Event{term.K('c'), term.K('o'), term.K('d'), term.K('e')}, 207 WantNewState: CodeAreaState{Buffer: CodeBuffer{Content: "code", Dot: 4}}, 208 }, 209 { 210 Name: "unicode inserts", 211 Given: NewCodeArea(CodeAreaSpec{}), 212 Events: []term.Event{term.K('你'), term.K('好')}, 213 WantNewState: CodeAreaState{Buffer: CodeBuffer{Content: "你好", Dot: 6}}, 214 }, 215 { 216 Name: "unterminated paste", 217 Given: NewCodeArea(CodeAreaSpec{}), 218 Events: []term.Event{term.PasteSetting(true), term.K('"'), term.K('x')}, 219 WantNewState: CodeAreaState{}, 220 }, 221 { 222 Name: "literal paste", 223 Given: NewCodeArea(CodeAreaSpec{}), 224 Events: []term.Event{ 225 term.PasteSetting(true), 226 term.K('"'), term.K('x'), 227 term.PasteSetting(false)}, 228 WantNewState: CodeAreaState{Buffer: CodeBuffer{Content: "\"x", Dot: 2}}, 229 }, 230 { 231 Name: "literal paste swallowing functional keys", 232 Given: NewCodeArea(CodeAreaSpec{}), 233 Events: []term.Event{ 234 term.PasteSetting(true), 235 term.K('a'), term.K(ui.F1), term.K('b'), 236 term.PasteSetting(false)}, 237 WantNewState: CodeAreaState{Buffer: CodeBuffer{Content: "ab", Dot: 2}}, 238 }, 239 { 240 Name: "quoted paste", 241 Given: NewCodeArea(CodeAreaSpec{QuotePaste: func() bool { return true }}), 242 Events: []term.Event{ 243 term.PasteSetting(true), 244 term.K('"'), term.K('x'), 245 term.PasteSetting(false)}, 246 WantNewState: CodeAreaState{Buffer: CodeBuffer{Content: "'\"x'", Dot: 4}}, 247 }, 248 { 249 Name: "backspace at end of code", 250 Given: NewCodeArea(CodeAreaSpec{}), 251 Events: []term.Event{ 252 term.K('c'), term.K('o'), term.K('d'), term.K('e'), 253 term.K(ui.Backspace)}, 254 WantNewState: CodeAreaState{Buffer: CodeBuffer{Content: "cod", Dot: 3}}, 255 }, 256 { 257 Name: "backspace at middle of buffer", 258 Given: NewCodeArea(CodeAreaSpec{State: CodeAreaState{ 259 Buffer: CodeBuffer{Content: "code", Dot: 2}}}), 260 Events: []term.Event{term.K(ui.Backspace)}, 261 WantNewState: CodeAreaState{Buffer: CodeBuffer{Content: "cde", Dot: 1}}, 262 }, 263 { 264 Name: "backspace at beginning of buffer", 265 Given: NewCodeArea(CodeAreaSpec{State: CodeAreaState{ 266 Buffer: CodeBuffer{Content: "code", Dot: 0}}}), 267 Events: []term.Event{term.K(ui.Backspace)}, 268 WantNewState: CodeAreaState{Buffer: CodeBuffer{Content: "code", Dot: 0}}, 269 }, 270 { 271 Name: "backspace deleting unicode character", 272 Given: NewCodeArea(CodeAreaSpec{}), 273 Events: []term.Event{ 274 term.K('你'), term.K('好'), term.K(ui.Backspace)}, 275 WantNewState: CodeAreaState{Buffer: CodeBuffer{Content: "你", Dot: 3}}, 276 }, 277 // Regression test for https://b.elv.sh/1178 278 { 279 Name: "Ctrl-H being equivalent to backspace", 280 Given: NewCodeArea(CodeAreaSpec{}), 281 Events: []term.Event{ 282 term.K('c'), term.K('o'), term.K('d'), term.K('e'), 283 term.K('H', ui.Ctrl)}, 284 WantNewState: CodeAreaState{Buffer: CodeBuffer{Content: "cod", Dot: 3}}, 285 }, 286 { 287 Name: "abbreviation expansion", 288 Given: NewCodeArea(CodeAreaSpec{ 289 SimpleAbbreviations: func(f func(abbr, full string)) { 290 f("dn", "/dev/null") 291 }, 292 }), 293 Events: []term.Event{term.K('d'), term.K('n')}, 294 WantNewState: CodeAreaState{Buffer: CodeBuffer{Content: "/dev/null", Dot: 9}}, 295 }, 296 { 297 Name: "abbreviation expansion 2", 298 Given: NewCodeArea(CodeAreaSpec{ 299 SimpleAbbreviations: func(f func(abbr, full string)) { 300 f("||", " | less") 301 }, 302 }), 303 Events: []term.Event{term.K('x'), term.K('|'), term.K('|')}, 304 WantNewState: CodeAreaState{Buffer: CodeBuffer{Content: "x | less", Dot: 8}}, 305 }, 306 { 307 Name: "abbreviation expansion after other content", 308 Given: NewCodeArea(CodeAreaSpec{ 309 SimpleAbbreviations: func(f func(abbr, full string)) { 310 f("||", " | less") 311 }, 312 }), 313 Events: []term.Event{term.K('{'), term.K('e'), term.K('c'), term.K('h'), term.K('o'), term.K(' '), term.K('x'), term.K('}'), term.K('|'), term.K('|')}, 314 WantNewState: CodeAreaState{Buffer: CodeBuffer{Content: "{echo x} | less", Dot: 15}}, 315 }, 316 { 317 Name: "abbreviation expansion preferring longest", 318 Given: NewCodeArea(CodeAreaSpec{ 319 SimpleAbbreviations: func(f func(abbr, full string)) { 320 f("n", "none") 321 f("dn", "/dev/null") 322 }, 323 }), 324 Events: []term.Event{term.K('d'), term.K('n')}, 325 WantNewState: CodeAreaState{Buffer: CodeBuffer{Content: "/dev/null", Dot: 9}}, 326 }, 327 { 328 Name: "abbreviation expansion interrupted by function key", 329 Given: NewCodeArea(CodeAreaSpec{ 330 SimpleAbbreviations: func(f func(abbr, full string)) { 331 f("dn", "/dev/null") 332 }, 333 }), 334 Events: []term.Event{term.K('d'), term.K(ui.F1), term.K('n')}, 335 WantNewState: CodeAreaState{Buffer: CodeBuffer{Content: "dn", Dot: 2}}, 336 }, 337 { 338 Name: "small word abbreviation expansion space trigger", 339 Given: NewCodeArea(CodeAreaSpec{ 340 SmallWordAbbreviations: func(f func(abbr, full string)) { 341 f("eh", "echo hello") 342 }, 343 }), 344 Events: []term.Event{term.K('e'), term.K('h'), term.K(' ')}, 345 WantNewState: CodeAreaState{Buffer: CodeBuffer{Content: "echo hello ", Dot: 11}}, 346 }, 347 { 348 Name: "small word abbreviation expansion non-space trigger", 349 Given: NewCodeArea(CodeAreaSpec{ 350 SmallWordAbbreviations: func(f func(abbr, full string)) { 351 f("h", "hello") 352 }, 353 }), 354 Events: []term.Event{term.K('x'), term.K('['), term.K('h'), term.K(']')}, 355 WantNewState: CodeAreaState{Buffer: CodeBuffer{Content: "x[hello]", Dot: 8}}, 356 }, 357 { 358 Name: "small word abbreviation expansion preceding char invalid", 359 Given: NewCodeArea(CodeAreaSpec{ 360 SmallWordAbbreviations: func(f func(abbr, full string)) { 361 f("h", "hello") 362 }, 363 }), 364 Events: []term.Event{term.K('g'), term.K('h'), term.K(' ')}, 365 WantNewState: CodeAreaState{Buffer: CodeBuffer{Content: "gh ", Dot: 3}}, 366 }, 367 { 368 Name: "small word abbreviation expansion after backspace preceding char invalid", 369 Given: NewCodeArea(CodeAreaSpec{ 370 SmallWordAbbreviations: func(f func(abbr, full string)) { 371 f("h", "hello") 372 }, 373 }), 374 Events: []term.Event{term.K('g'), term.K(' '), term.K(ui.Backspace), 375 term.K('h'), term.K(' ')}, 376 WantNewState: CodeAreaState{Buffer: CodeBuffer{Content: "gh ", Dot: 3}}, 377 }, 378 { 379 Name: "command abbreviation expansion", 380 Given: NewCodeArea(CodeAreaSpec{ 381 CommandAbbreviations: func(f func(abbr, full string)) { 382 f("eh", "echo hello") 383 }, 384 }), 385 Events: []term.Event{term.K('e'), term.K('h'), term.K(' ')}, 386 WantNewState: CodeAreaState{Buffer: CodeBuffer{Content: "echo hello ", Dot: 11}}, 387 }, 388 { 389 Name: "command abbreviation expansion not at start of line", 390 Given: NewCodeArea(CodeAreaSpec{ 391 CommandAbbreviations: func(f func(abbr, full string)) { 392 f("eh", "echo hello") 393 }, 394 }), 395 Events: []term.Event{term.K('x'), term.K('|'), term.K('e'), term.K('h'), term.K(' ')}, 396 WantNewState: CodeAreaState{Buffer: CodeBuffer{Content: "x|echo hello ", Dot: 13}}, 397 }, 398 { 399 Name: "command abbreviation expansion at start of second line", 400 Given: NewCodeArea(CodeAreaSpec{ 401 CommandAbbreviations: func(f func(abbr, full string)) { 402 f("eh", "echo hello") 403 }, 404 State: CodeAreaState{Buffer: CodeBuffer{Content: "echo\n", Dot: 5}}, 405 }), 406 Events: []term.Event{term.K('e'), term.K('h'), term.K(' ')}, 407 WantNewState: CodeAreaState{Buffer: CodeBuffer{Content: "echo\necho hello ", Dot: 16}}, 408 }, 409 { 410 Name: "no command abbreviation expansion when not in command position", 411 Given: NewCodeArea(CodeAreaSpec{ 412 CommandAbbreviations: func(f func(abbr, full string)) { 413 f("eh", "echo hello") 414 }, 415 }), 416 Events: []term.Event{term.K('x'), term.K(' '), term.K('e'), term.K('h'), term.K(' ')}, 417 WantNewState: CodeAreaState{Buffer: CodeBuffer{Content: "x eh ", Dot: 5}}, 418 }, 419 { 420 Name: "key bindings", 421 Given: NewCodeArea(CodeAreaSpec{Bindings: MapBindings{ 422 term.K('a'): func(w Widget) { 423 w.(*codeArea).State.Buffer.InsertAtDot("b") 424 }}, 425 }), 426 Events: []term.Event{term.K('a')}, 427 WantNewState: CodeAreaState{Buffer: CodeBuffer{Content: "b", Dot: 1}}, 428 }, 429 { 430 // Regression test for #890. 431 Name: "key bindings do not apply when pasting", 432 Given: NewCodeArea(CodeAreaSpec{Bindings: MapBindings{ 433 term.K('\n'): func(w Widget) {}}, 434 }), 435 Events: []term.Event{ 436 term.PasteSetting(true), term.K('\n'), term.PasteSetting(false)}, 437 WantNewState: CodeAreaState{Buffer: CodeBuffer{Content: "\n", Dot: 1}}, 438 }, 439 } 440 441 func TestCodeArea_Handle(t *testing.T) { 442 testHandle(t, codeAreaHandleTests) 443 } 444 445 var codeAreaUnhandledEvents = []term.Event{ 446 // Mouse events are unhandled 447 term.MouseEvent{}, 448 // Function keys are unhandled (except Backspace) 449 term.K(ui.F1), 450 term.K('X', ui.Ctrl), 451 } 452 453 func TestCodeArea_Handle_UnhandledEvents(t *testing.T) { 454 w := NewCodeArea(CodeAreaSpec{}) 455 for _, event := range codeAreaUnhandledEvents { 456 handled := w.Handle(event) 457 if handled { 458 t.Errorf("event %v got handled", event) 459 } 460 } 461 } 462 463 func TestCodeArea_Handle_AbbreviationExpansionInterruptedByExternalMutation(t *testing.T) { 464 w := NewCodeArea(CodeAreaSpec{ 465 SimpleAbbreviations: func(f func(abbr, full string)) { 466 f("dn", "/dev/null") 467 }, 468 }) 469 w.Handle(term.K('d')) 470 w.MutateState(func(s *CodeAreaState) { s.Buffer.InsertAtDot("d") }) 471 w.Handle(term.K('n')) 472 wantState := CodeAreaState{Buffer: CodeBuffer{Content: "ddn", Dot: 3}} 473 if state := w.CopyState(); !reflect.DeepEqual(state, wantState) { 474 t.Errorf("got state %v, want %v", state, wantState) 475 } 476 } 477 478 func TestCodeArea_Handle_EnterEmitsSubmit(t *testing.T) { 479 submitted := false 480 w := NewCodeArea(CodeAreaSpec{ 481 OnSubmit: func() { submitted = true }, 482 State: CodeAreaState{Buffer: CodeBuffer{Content: "code", Dot: 4}}}) 483 w.Handle(term.K('\n')) 484 if submitted != true { 485 t.Errorf("OnSubmit not triggered") 486 } 487 } 488 489 func TestCodeArea_Handle_DefaultNoopSubmit(t *testing.T) { 490 w := NewCodeArea(CodeAreaSpec{State: CodeAreaState{ 491 Buffer: CodeBuffer{Content: "code", Dot: 4}}}) 492 w.Handle(term.K('\n')) 493 // No panic, we are good 494 } 495 496 func TestCodeArea_State(t *testing.T) { 497 w := NewCodeArea(CodeAreaSpec{}) 498 w.MutateState(func(s *CodeAreaState) { s.Buffer.Content = "code" }) 499 if w.CopyState().Buffer.Content != "code" { 500 t.Errorf("state not mutated") 501 } 502 } 503 504 func TestCodeAreaState_ApplyPending(t *testing.T) { 505 applyPending := func(s CodeAreaState) CodeAreaState { 506 s.ApplyPending() 507 return s 508 } 509 tt.Test(t, applyPending, 510 Args(CodeAreaState{Buffer: CodeBuffer{}, Pending: PendingCode{0, 0, "ls"}}). 511 Rets(CodeAreaState{Buffer: CodeBuffer{Content: "ls", Dot: 2}, Pending: PendingCode{}}), 512 Args(CodeAreaState{Buffer: CodeBuffer{"x", 1}, Pending: PendingCode{0, 0, "ls"}}). 513 Rets(CodeAreaState{Buffer: CodeBuffer{Content: "lsx", Dot: 3}, Pending: PendingCode{}}), 514 // No-op when Pending is empty. 515 Args(CodeAreaState{Buffer: CodeBuffer{"x", 1}}). 516 Rets(CodeAreaState{Buffer: CodeBuffer{Content: "x", Dot: 1}}), 517 // HideRPrompt is kept intact. 518 Args(CodeAreaState{Buffer: CodeBuffer{"x", 1}, HideRPrompt: true}). 519 Rets(CodeAreaState{Buffer: CodeBuffer{Content: "x", Dot: 1}, HideRPrompt: true}), 520 ) 521 }