| 🛟 Updated. 28d5985 k33g 19h ago | 1 | package editor |
| 2 | |
| 3 | import ( |
| 4 | "strings" |
| 5 | "testing" |
| 6 | |
| 7 | "github.com/gdamore/tcell/v2" |
| 8 | |
| 📦 Turbo Core f3ade8d k33g 11h ago | 9 | "rickub.com/turbo-editors/turbo-core/buffer" |
| 🛟 Updated. 28d5985 k33g 19h ago | 10 | ) |
| 11 | |
| 12 | func TestArrowKeysMoveTheCursor(t *testing.T) { |
| 13 | view, _, _ := newTestView(t, "abc\ndef", 40, 6) |
| 14 | |
| 15 | view.HandleKey(key(tcell.KeyRight, tcell.ModNone)) |
| 16 | view.HandleKey(key(tcell.KeyDown, tcell.ModNone)) |
| 17 | |
| 18 | if got := view.Buffer().Cursor(); got != (buffer.Position{Line: 1, Col: 1}) { |
| 19 | t.Errorf("Cursor() = %+v, want {Line:1 Col:1}", got) |
| 20 | } |
| 21 | } |
| 22 | |
| 23 | func TestCtrlArrowsMoveByWord(t *testing.T) { |
| 24 | view, _, _ := newTestView(t, "alpha beta gamma", 40, 6) |
| 25 | |
| 26 | view.HandleKey(key(tcell.KeyRight, tcell.ModCtrl)) |
| 27 | |
| 28 | if got := view.Buffer().Cursor().Col; got != 6 { |
| 29 | t.Errorf("Cursor().Col = %d, want the start of the second word", got) |
| 30 | } |
| 31 | } |
| 32 | |
| 33 | func TestHomeAndEnd(t *testing.T) { |
| 34 | view, _, _ := newTestView(t, "abc\ndefgh", 40, 6) |
| 35 | view.Buffer().SetCursor(buffer.Position{Line: 1, Col: 2}) |
| 36 | |
| 37 | view.HandleKey(key(tcell.KeyEnd, tcell.ModNone)) |
| 38 | if got := view.Buffer().Cursor().Col; got != 5 { |
| 39 | t.Errorf("End left the cursor at column %d, want 5", got) |
| 40 | } |
| 41 | |
| 42 | view.HandleKey(key(tcell.KeyHome, tcell.ModNone)) |
| 43 | if got := view.Buffer().Cursor().Col; got != 0 { |
| 44 | t.Errorf("Home left the cursor at column %d, want 0", got) |
| 45 | } |
| 46 | |
| 47 | view.HandleKey(key(tcell.KeyHome, tcell.ModCtrl)) |
| 48 | if got := view.Buffer().Cursor(); got != (buffer.Position{}) { |
| 49 | t.Errorf("Ctrl-Home left the cursor at %+v, want the start of the buffer", got) |
| 50 | } |
| 51 | |
| 52 | view.HandleKey(key(tcell.KeyEnd, tcell.ModCtrl)) |
| 53 | if got := view.Buffer().Cursor(); got != (buffer.Position{Line: 1, Col: 5}) { |
| 54 | t.Errorf("Ctrl-End left the cursor at %+v, want the end of the buffer", got) |
| 55 | } |
| 56 | } |
| 57 | |
| 58 | func TestPageKeysMoveByAScreenful(t *testing.T) { |
| 59 | view, _, _ := newTestView(t, strings.Repeat("line\n", 100), 40, 12) |
| 60 | page := view.VisibleLines() |
| 61 | |
| 62 | view.HandleKey(key(tcell.KeyPgDn, tcell.ModNone)) |
| 63 | |
| 64 | if got := view.Buffer().Cursor().Line; got != page { |
| 65 | t.Errorf("Cursor().Line = %d, want %d", got, page) |
| 66 | } |
| 67 | |
| 68 | view.HandleKey(key(tcell.KeyPgUp, tcell.ModNone)) |
| 69 | if got := view.Buffer().Cursor().Line; got != 0 { |
| 70 | t.Errorf("Cursor().Line = %d, want 0", got) |
| 71 | } |
| 72 | } |
| 73 | |
| 74 | func TestShiftArrowsExtendTheSelection(t *testing.T) { |
| 75 | view, _, _ := newTestView(t, "hello world", 40, 6) |
| 76 | |
| 77 | for range 5 { |
| 78 | view.HandleKey(key(tcell.KeyRight, tcell.ModShift)) |
| 79 | } |
| 80 | |
| 81 | if got := view.SelectedText(); got != "hello" { |
| 82 | t.Errorf("SelectedText() = %q, want %q", got, "hello") |
| 83 | } |
| 84 | } |
| 85 | |
| 86 | func TestAPlainArrowDropsTheSelection(t *testing.T) { |
| 87 | view, _, _ := newTestView(t, "hello world", 40, 6) |
| 88 | view.HandleKey(key(tcell.KeyRight, tcell.ModShift)) |
| 89 | |
| 90 | view.HandleKey(key(tcell.KeyRight, tcell.ModNone)) |
| 91 | |
| 92 | if got := view.SelectedText(); got != "" { |
| 93 | t.Errorf("SelectedText() = %q, want the selection dropped", got) |
| 94 | } |
| 95 | } |
| 96 | |
| 97 | func TestTypingInsertsCharacters(t *testing.T) { |
| 98 | view, _, _ := newTestView(t, "", 40, 6) |
| 99 | changes := 0 |
| 100 | view.OnChange = func() { changes++ } |
| 101 | |
| 102 | for _, r := range "func" { |
| 103 | view.HandleKey(typed(r)) |
| 104 | } |
| 105 | |
| 106 | if got := view.Buffer().Text(); got != "func" { |
| 107 | t.Errorf("Text() = %q, want %q", got, "func") |
| 108 | } |
| 109 | if changes != 4 { |
| 110 | t.Errorf("OnChange was called %d times, want 4", changes) |
| 111 | } |
| 112 | } |
| 113 | |
| 114 | func TestCtrlAndAltLettersAreNotTyped(t *testing.T) { |
| 115 | view, _, _ := newTestView(t, "", 40, 6) |
| 116 | |
| 117 | view.HandleKey(tcell.NewEventKey(tcell.KeyRune, 'x', tcell.ModAlt)) |
| 118 | |
| 119 | if got := view.Buffer().Text(); got != "" { |
| 120 | t.Errorf("Text() = %q, want a modified key not to have been typed", got) |
| 121 | } |
| 122 | } |
| 123 | |
| 124 | func TestEnterInsertsALineAndKeepsTheIndent(t *testing.T) { |
| 125 | view, _, _ := newTestView(t, "\tfoo()", 40, 6) |
| 126 | view.Buffer().MoveLineEnd() |
| 127 | |
| 128 | view.HandleKey(key(tcell.KeyEnter, tcell.ModNone)) |
| 129 | |
| 130 | if got := view.Buffer().Line(1); got != "\t" { |
| 131 | t.Errorf("Line(1) = %q, want the indent carried over", got) |
| 132 | } |
| 133 | } |
| 134 | |
| 135 | func TestBackspaceAndDelete(t *testing.T) { |
| 136 | view, _, _ := newTestView(t, "abcd", 40, 6) |
| 137 | view.Buffer().SetCursor(buffer.Position{Line: 0, Col: 2}) |
| 138 | |
| 139 | view.HandleKey(key(tcell.KeyBackspace2, tcell.ModNone)) |
| 140 | view.HandleKey(key(tcell.KeyDelete, tcell.ModNone)) |
| 141 | |
| 142 | if got := view.Buffer().Text(); got != "ad" { |
| 143 | t.Errorf("Text() = %q, want %q", got, "ad") |
| 144 | } |
| 145 | } |
| 146 | |
| 147 | func TestTabInsertsATabWhenNothingIsSelected(t *testing.T) { |
| 148 | view, _, _ := newTestView(t, "x", 40, 6) |
| 149 | |
| 150 | view.HandleKey(key(tcell.KeyTab, tcell.ModNone)) |
| 151 | |
| 152 | if got := view.Buffer().Text(); got != "\tx" { |
| 153 | t.Errorf("Text() = %q, want a tab inserted", got) |
| 154 | } |
| 155 | } |
| 156 | |
| 157 | func TestTabIndentsEveryLineOfTheSelection(t *testing.T) { |
| 158 | view, _, _ := newTestView(t, "one\ntwo\nthree", 40, 6) |
| 159 | view.Buffer().SetCursor(buffer.Position{Line: 0, Col: 1}) |
| 160 | view.Buffer().StartSelection() |
| 161 | view.Buffer().SetCursorKeepingSelection(buffer.Position{Line: 1, Col: 1}) |
| 162 | |
| 163 | view.HandleKey(key(tcell.KeyTab, tcell.ModNone)) |
| 164 | |
| 165 | if got := view.Buffer().Text(); got != "\tone\n\ttwo\nthree" { |
| 166 | t.Errorf("Text() = %q, want the first two lines indented", got) |
| 167 | } |
| 168 | } |
| 169 | |
| 170 | func TestShiftTabRemovesOneIndentLevel(t *testing.T) { |
| 171 | tests := []struct { |
| 172 | name string |
| 173 | give string |
| 174 | want string |
| 175 | }{ |
| 176 | {"a tab", "\tx", "x"}, |
| 177 | {"eight spaces", " x", "x"}, |
| 178 | {"three spaces", " x", "x"}, |
| 179 | {"nothing to remove", "x", "x"}, |
| 180 | } |
| 181 | |
| 182 | for _, tc := range tests { |
| 183 | t.Run(tc.name, func(t *testing.T) { |
| 184 | view, _, _ := newTestView(t, tc.give, 40, 6) |
| 185 | |
| 186 | view.HandleKey(key(tcell.KeyBacktab, tcell.ModNone)) |
| 187 | |
| 188 | if got := view.Buffer().Text(); got != tc.want { |
| 189 | t.Errorf("Text() = %q, want %q", got, tc.want) |
| 190 | } |
| 191 | }) |
| 192 | } |
| 193 | } |
| 194 | |
| 195 | func TestCutCopyAndPaste(t *testing.T) { |
| 196 | clipboard := &Clipboard{} |
| 197 | view, _, _ := newTestView(t, "hello world", 40, 6) |
| 198 | view.clipboard = clipboard |
| 199 | |
| 200 | view.Buffer().SetCursor(buffer.Position{Line: 0, Col: 0}) |
| 201 | view.Buffer().StartSelection() |
| 202 | view.Buffer().SetCursorKeepingSelection(buffer.Position{Line: 0, Col: 6}) |
| 203 | |
| 204 | view.HandleKey(key(tcell.KeyCtrlX, tcell.ModNone)) |
| 205 | if got := view.Buffer().Text(); got != "world" { |
| 206 | t.Fatalf("Text() = %q after a cut, want %q", got, "world") |
| 207 | } |
| 208 | if got := clipboard.Text(); got != "hello " { |
| 209 | t.Errorf("the clipboard holds %q, want %q", got, "hello ") |
| 210 | } |
| 211 | |
| 212 | view.Buffer().MoveBufferEnd() |
| 213 | view.HandleKey(key(tcell.KeyCtrlV, tcell.ModNone)) |
| 214 | if got := view.Buffer().Text(); got != "worldhello " { |
| 215 | t.Errorf("Text() = %q after a paste, want %q", got, "worldhello ") |
| 216 | } |
| 217 | } |
| 218 | |
| 219 | func TestTheTurboCClipboardKeysWorkToo(t *testing.T) { |
| 220 | clipboard := &Clipboard{} |
| 221 | view, _, _ := newTestView(t, "abc", 40, 6) |
| 222 | view.clipboard = clipboard |
| 223 | view.SelectAll() |
| 224 | |
| 225 | view.HandleKey(key(tcell.KeyInsert, tcell.ModCtrl)) // Ctrl-Ins copies |
| 226 | if got := clipboard.Text(); got != "abc" { |
| 227 | t.Errorf("the clipboard holds %q, want %q", got, "abc") |
| 228 | } |
| 229 | |
| 230 | view.Buffer().MoveBufferEnd() |
| 231 | view.HandleKey(key(tcell.KeyInsert, tcell.ModShift)) // Shift-Ins pastes |
| 232 | if got := view.Buffer().Text(); got != "abcabc" { |
| 233 | t.Errorf("Text() = %q, want %q", got, "abcabc") |
| 234 | } |
| 235 | |
| 236 | view.SelectAll() |
| 237 | view.HandleKey(key(tcell.KeyDelete, tcell.ModShift)) // Shift-Del cuts |
| 238 | if got := view.Buffer().Text(); got != "" { |
| 239 | t.Errorf("Text() = %q, want it emptied by the cut", got) |
| 240 | } |
| 241 | } |
| 242 | |
| 243 | func TestCopyingNothingDoesNotTouchTheClipboard(t *testing.T) { |
| 244 | clipboard := &Clipboard{} |
| 245 | clipboard.SetText("kept") |
| 246 | view, _, _ := newTestView(t, "abc", 40, 6) |
| 247 | view.clipboard = clipboard |
| 248 | |
| 249 | if view.Copy() || view.Cut() { |
| 250 | t.Error("Copy or Cut reported success with nothing selected") |
| 251 | } |
| 252 | if got := clipboard.Text(); got != "kept" { |
| 253 | t.Errorf("the clipboard holds %q, want it untouched", got) |
| 254 | } |
| 255 | } |
| 256 | |
| 257 | func TestPastingAnEmptyClipboardDoesNothing(t *testing.T) { |
| 258 | view, _, _ := newTestView(t, "abc", 40, 6) |
| 259 | |
| 260 | if view.Paste() { |
| 261 | t.Error("Paste reported success with an empty clipboard") |
| 262 | } |
| 263 | if got := view.Buffer().Text(); got != "abc" { |
| 264 | t.Errorf("Text() = %q, want it unchanged", got) |
| 265 | } |
| 266 | } |
| 267 | |
| 268 | func TestUndoAndRedoKeys(t *testing.T) { |
| 269 | view, _, _ := newTestView(t, "abc", 40, 6) |
| 270 | view.Buffer().MoveBufferEnd() |
| 271 | view.HandleKey(typed('d')) |
| 272 | |
| 273 | view.HandleKey(key(tcell.KeyCtrlZ, tcell.ModNone)) |
| 274 | if got := view.Buffer().Text(); got != "abc" { |
| 275 | t.Errorf("Text() = %q after undo, want %q", got, "abc") |
| 276 | } |
| 277 | |
| 278 | // Ctrl-R, not Ctrl-Y: Ctrl-Y is Turbo C's delete-line, and that claim won. |
| 279 | view.HandleKey(key(tcell.KeyCtrlR, tcell.ModNone)) |
| 280 | if got := view.Buffer().Text(); got != "abcd" { |
| 281 | t.Errorf("Text() = %q after redo, want %q", got, "abcd") |
| 282 | } |
| 283 | } |
| 284 | |
| 285 | func TestCtrlYNoLongerRedoes(t *testing.T) { |
| 286 | // The half of the change that a user notices: the old key must not |
| 287 | // quietly keep working, or nobody learns the new one until it bites. |
| 288 | view, _, _ := newTestView(t, "abc", 40, 6) |
| 289 | view.Buffer().MoveBufferEnd() |
| 290 | view.HandleKey(typed('d')) |
| 291 | view.HandleKey(key(tcell.KeyCtrlZ, tcell.ModNone)) |
| 292 | |
| 293 | view.HandleKey(key(tcell.KeyCtrlY, tcell.ModNone)) |
| 294 | |
| 295 | if got := view.Buffer().Text(); got == "abcd" { |
| 296 | t.Error("Ctrl-Y redid the change; it deletes a line now") |
| 297 | } |
| 298 | } |
| 299 | |
| 300 | func TestCtrlNInsertsALineAndCtrlYDeletesOne(t *testing.T) { |
| 301 | view, _, _ := newTestView(t, "one\ntwo\nthree\n", 40, 6) |
| 302 | view.Buffer().SetCursor(buffer.Position{Line: 1, Col: 1}) |
| 303 | |
| 304 | view.HandleKey(key(tcell.KeyCtrlN, tcell.ModNone)) |
| 305 | if got := view.Buffer().Text(); got != "one\n\ntwo\nthree\n" { |
| 306 | t.Fatalf("after Ctrl-N Text() = %q", got) |
| 307 | } |
| 308 | |
| 309 | view.HandleKey(key(tcell.KeyCtrlY, tcell.ModNone)) |
| 310 | if got := view.Buffer().Text(); got != "one\n\nthree\n" { |
| 311 | t.Errorf("after Ctrl-Y Text() = %q, want the line the cursor was on gone", got) |
| 312 | } |
| 313 | } |
| 314 | |
| 315 | func TestCtrlASelectsEverything(t *testing.T) { |
| 316 | view, _, _ := newTestView(t, "one\ntwo", 40, 6) |
| 317 | |
| 318 | view.HandleKey(key(tcell.KeyCtrlA, tcell.ModNone)) |
| 319 | |
| 320 | if got := view.SelectedText(); got != "one\ntwo" { |
| 321 | t.Errorf("SelectedText() = %q, want the whole buffer", got) |
| 322 | } |
| 323 | } |
| 324 | |
| 325 | func TestCtrlSpaceAsksForCompletion(t *testing.T) { |
| 326 | view, _, _ := newTestView(t, "fmt.", 40, 6) |
| 327 | asked := 0 |
| 328 | view.OnCompletionRequest = func() { asked++ } |
| 329 | |
| 330 | view.HandleKey(key(tcell.KeyCtrlSpace, tcell.ModNone)) |
| 331 | |
| 332 | if asked != 1 { |
| 333 | t.Errorf("completion was asked for %d times, want 1", asked) |
| 334 | } |
| 335 | } |
| 336 | |
| 337 | func TestTypingADotAsksForCompletion(t *testing.T) { |
| 338 | view, _, _ := newTestView(t, "fmt", 40, 6) |
| 339 | view.Buffer().MoveBufferEnd() |
| 340 | asked := 0 |
| 341 | view.OnCompletionRequest = func() { asked++ } |
| 342 | |
| 343 | view.HandleKey(typed('.')) |
| 344 | view.HandleKey(typed('P')) |
| 345 | |
| 346 | if asked != 1 { |
| 347 | t.Errorf("completion was asked for %d times, want 1 — only the dot triggers it", asked) |
| 348 | } |
| 349 | } |
| 350 | |
| 351 | func TestAKeyTheViewHasNoUseForIsPassedOn(t *testing.T) { |
| 352 | view, _, _ := newTestView(t, "abc", 40, 6) |
| 353 | |
| 354 | if view.HandleKey(key(tcell.KeyF9, tcell.ModNone)) { |
| 355 | t.Error("the view claimed a function key it does nothing with") |
| 356 | } |
| 357 | } |
| 358 | |
| 359 | func TestWordBeforeCursor(t *testing.T) { |
| 360 | tests := []struct { |
| 361 | name string |
| 362 | text string |
| 363 | col int |
| 364 | want string |
| 365 | }{ |
| 366 | {"mid-identifier", "fmt.Print", 9, "Print"}, |
| 367 | {"just after a dot", "fmt.", 4, ""}, |
| 368 | {"at the start of a line", "abc", 0, ""}, |
| 369 | {"a whole word", "hello", 5, "hello"}, |
| 370 | {"with digits and underscores", "a_1b", 4, "a_1b"}, |
| 371 | } |
| 372 | |
| 373 | for _, tc := range tests { |
| 374 | t.Run(tc.name, func(t *testing.T) { |
| 375 | view, _, _ := newTestView(t, tc.text, 40, 6) |
| 376 | view.Buffer().SetCursor(buffer.Position{Line: 0, Col: tc.col}) |
| 377 | |
| 378 | if got := view.WordBeforeCursor(); got != tc.want { |
| 379 | t.Errorf("WordBeforeCursor() = %q, want %q", got, tc.want) |
| 380 | } |
| 381 | }) |
| 382 | } |
| 383 | } |
| 384 | |
| 385 | func TestReplaceWordBeforeCursorAcceptsACompletion(t *testing.T) { |
| 386 | view, _, _ := newTestView(t, "fmt.Prin", 40, 6) |
| 387 | view.Buffer().MoveBufferEnd() |
| 388 | |
| 389 | view.ReplaceWordBeforeCursor("Println") |
| 390 | |
| 391 | if got := view.Buffer().Text(); got != "fmt.Println" { |
| 392 | t.Errorf("Text() = %q, want %q", got, "fmt.Println") |
| 393 | } |
| 394 | } |
| 395 | |
| 396 | func TestGoToLine(t *testing.T) { |
| 397 | view, _, _ := newTestView(t, strings.Repeat("x\n", 50), 40, 8) |
| 398 | |
| 399 | view.GoToLine(30) |
| 400 | |
| 401 | if got := view.Buffer().Cursor().Line; got != 29 { |
| 402 | t.Errorf("Cursor().Line = %d, want 29 — GoToLine counts from one", got) |
| 403 | } |
| 404 | if top := view.TopLine(); 29 < top || 29 >= top+view.VisibleLines() { |
| 405 | t.Error("the view did not scroll to the requested line") |
| 406 | } |
| 407 | } |
| 408 | |
| 409 | func TestClickingPlacesTheCursor(t *testing.T) { |
| 410 | view, _, _ := newTestView(t, "package main\nfunc main() {}", 40, 8) |
| 411 | gutter := view.gutterWidth() |
| 412 | |
| 413 | view.HandleMouse(tcell.NewEventMouse(gutter+4, 1, tcell.Button1, tcell.ModNone)) |
| 414 | |
| 415 | if got := view.Buffer().Cursor(); got != (buffer.Position{Line: 1, Col: 4}) { |
| 416 | t.Errorf("Cursor() = %+v, want {Line:1 Col:4}", got) |
| 417 | } |
| 418 | } |
| 419 | |
| 420 | func TestDraggingSelects(t *testing.T) { |
| 421 | view, _, _ := newTestView(t, "hello world", 40, 8) |
| 422 | gutter := view.gutterWidth() |
| 423 | |
| 424 | view.HandleMouse(tcell.NewEventMouse(gutter, 0, tcell.Button1, tcell.ModNone)) |
| 425 | view.HandleMouse(tcell.NewEventMouse(gutter+5, 0, tcell.Button1, tcell.ModNone)) |
| 426 | |
| 427 | if got := view.SelectedText(); got != "hello" { |
| 428 | t.Errorf("SelectedText() = %q, want %q", got, "hello") |
| 429 | } |
| 430 | |
| 431 | view.HandleMouse(tcell.NewEventMouse(gutter+5, 0, tcell.ButtonNone, tcell.ModNone)) |
| 432 | if view.selecting { |
| 433 | t.Error("the drag did not end when the button came back up") |
| 434 | } |
| 435 | } |
| 436 | |
| 437 | func TestTheWheelScrolls(t *testing.T) { |
| 438 | view, _, _ := newTestView(t, strings.Repeat("line\n", 100), 40, 10) |
| 439 | |
| 440 | view.HandleMouse(tcell.NewEventMouse(5, 5, tcell.WheelDown, tcell.ModNone)) |
| 441 | |
| 442 | if got := view.TopLine(); got != wheelStep { |
| 443 | t.Errorf("TopLine() = %d, want %d", got, wheelStep) |
| 444 | } |
| 445 | |
| 446 | view.HandleMouse(tcell.NewEventMouse(5, 5, tcell.WheelUp, tcell.ModNone)) |
| 447 | if got := view.TopLine(); got != 0 { |
| 448 | t.Errorf("TopLine() = %d, want 0", got) |
| 449 | } |
| 450 | } |
| 451 | |
| 452 | func TestAMouseEventTheViewHasNoUseForIsPassedOn(t *testing.T) { |
| 453 | view, _, _ := newTestView(t, "abc", 40, 6) |
| 454 | |
| 455 | if view.HandleMouse(tcell.NewEventMouse(1, 1, tcell.Button3, tcell.ModNone)) { |
| 456 | t.Error("the view claimed a middle-button event it does nothing with") |
| 457 | } |
| 458 | } |
| 459 | |
| 460 | func TestTheCursorIsReportedWhenItMoves(t *testing.T) { |
| 461 | view, _, _ := newTestView(t, "abc", 40, 6) |
| 462 | moves := 0 |
| 463 | view.OnCursorMove = func() { moves++ } |
| 464 | |
| 465 | view.HandleKey(key(tcell.KeyRight, tcell.ModNone)) |
| 466 | view.HandleKey(typed('x')) // an edit moves the cursor too |
| 467 | |
| 468 | if moves != 2 { |
| 469 | t.Errorf("OnCursorMove was called %d times, want 2", moves) |
| 470 | } |
| 471 | } |