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