package editor import ( "strings" "testing" "github.com/gdamore/tcell/v2" "codeberg.org/turbo-editors/turbo-core/buffer" ) func TestArrowKeysMoveTheCursor(t *testing.T) { view, _, _ := newTestView(t, "abc\ndef", 40, 6) view.HandleKey(key(tcell.KeyRight, tcell.ModNone)) view.HandleKey(key(tcell.KeyDown, tcell.ModNone)) if got := view.Buffer().Cursor(); got != (buffer.Position{Line: 1, Col: 1}) { t.Errorf("Cursor() = %+v, want {Line:1 Col:1}", got) } } func TestCtrlArrowsMoveByWord(t *testing.T) { view, _, _ := newTestView(t, "alpha beta gamma", 40, 6) view.HandleKey(key(tcell.KeyRight, tcell.ModCtrl)) if got := view.Buffer().Cursor().Col; got != 6 { t.Errorf("Cursor().Col = %d, want the start of the second word", got) } } func TestHomeAndEnd(t *testing.T) { view, _, _ := newTestView(t, "abc\ndefgh", 40, 6) view.Buffer().SetCursor(buffer.Position{Line: 1, Col: 2}) view.HandleKey(key(tcell.KeyEnd, tcell.ModNone)) if got := view.Buffer().Cursor().Col; got != 5 { t.Errorf("End left the cursor at column %d, want 5", got) } view.HandleKey(key(tcell.KeyHome, tcell.ModNone)) if got := view.Buffer().Cursor().Col; got != 0 { t.Errorf("Home left the cursor at column %d, want 0", got) } view.HandleKey(key(tcell.KeyHome, tcell.ModCtrl)) if got := view.Buffer().Cursor(); got != (buffer.Position{}) { t.Errorf("Ctrl-Home left the cursor at %+v, want the start of the buffer", got) } view.HandleKey(key(tcell.KeyEnd, tcell.ModCtrl)) if got := view.Buffer().Cursor(); got != (buffer.Position{Line: 1, Col: 5}) { t.Errorf("Ctrl-End left the cursor at %+v, want the end of the buffer", got) } } func TestPageKeysMoveByAScreenful(t *testing.T) { view, _, _ := newTestView(t, strings.Repeat("line\n", 100), 40, 12) page := view.VisibleLines() view.HandleKey(key(tcell.KeyPgDn, tcell.ModNone)) if got := view.Buffer().Cursor().Line; got != page { t.Errorf("Cursor().Line = %d, want %d", got, page) } view.HandleKey(key(tcell.KeyPgUp, tcell.ModNone)) if got := view.Buffer().Cursor().Line; got != 0 { t.Errorf("Cursor().Line = %d, want 0", got) } } func TestShiftArrowsExtendTheSelection(t *testing.T) { view, _, _ := newTestView(t, "hello world", 40, 6) for range 5 { view.HandleKey(key(tcell.KeyRight, tcell.ModShift)) } if got := view.SelectedText(); got != "hello" { t.Errorf("SelectedText() = %q, want %q", got, "hello") } } func TestAPlainArrowDropsTheSelection(t *testing.T) { view, _, _ := newTestView(t, "hello world", 40, 6) view.HandleKey(key(tcell.KeyRight, tcell.ModShift)) view.HandleKey(key(tcell.KeyRight, tcell.ModNone)) if got := view.SelectedText(); got != "" { t.Errorf("SelectedText() = %q, want the selection dropped", got) } } func TestTypingInsertsCharacters(t *testing.T) { view, _, _ := newTestView(t, "", 40, 6) changes := 0 view.OnChange = func() { changes++ } for _, r := range "func" { view.HandleKey(typed(r)) } if got := view.Buffer().Text(); got != "func" { t.Errorf("Text() = %q, want %q", got, "func") } if changes != 4 { t.Errorf("OnChange was called %d times, want 4", changes) } } func TestCtrlAndAltLettersAreNotTyped(t *testing.T) { view, _, _ := newTestView(t, "", 40, 6) view.HandleKey(tcell.NewEventKey(tcell.KeyRune, 'x', tcell.ModAlt)) if got := view.Buffer().Text(); got != "" { t.Errorf("Text() = %q, want a modified key not to have been typed", got) } } func TestEnterInsertsALineAndKeepsTheIndent(t *testing.T) { view, _, _ := newTestView(t, "\tfoo()", 40, 6) view.Buffer().MoveLineEnd() view.HandleKey(key(tcell.KeyEnter, tcell.ModNone)) if got := view.Buffer().Line(1); got != "\t" { t.Errorf("Line(1) = %q, want the indent carried over", got) } } func TestBackspaceAndDelete(t *testing.T) { view, _, _ := newTestView(t, "abcd", 40, 6) view.Buffer().SetCursor(buffer.Position{Line: 0, Col: 2}) view.HandleKey(key(tcell.KeyBackspace2, tcell.ModNone)) view.HandleKey(key(tcell.KeyDelete, tcell.ModNone)) if got := view.Buffer().Text(); got != "ad" { t.Errorf("Text() = %q, want %q", got, "ad") } } func TestTabInsertsATabWhenNothingIsSelected(t *testing.T) { view, _, _ := newTestView(t, "x", 40, 6) view.HandleKey(key(tcell.KeyTab, tcell.ModNone)) if got := view.Buffer().Text(); got != "\tx" { t.Errorf("Text() = %q, want a tab inserted", got) } } func TestTabIndentsEveryLineOfTheSelection(t *testing.T) { view, _, _ := newTestView(t, "one\ntwo\nthree", 40, 6) view.Buffer().SetCursor(buffer.Position{Line: 0, Col: 1}) view.Buffer().StartSelection() view.Buffer().SetCursorKeepingSelection(buffer.Position{Line: 1, Col: 1}) view.HandleKey(key(tcell.KeyTab, tcell.ModNone)) if got := view.Buffer().Text(); got != "\tone\n\ttwo\nthree" { t.Errorf("Text() = %q, want the first two lines indented", got) } } func TestShiftTabRemovesOneIndentLevel(t *testing.T) { tests := []struct { name string give string want string }{ {"a tab", "\tx", "x"}, {"eight spaces", " x", "x"}, {"three spaces", " x", "x"}, {"nothing to remove", "x", "x"}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { view, _, _ := newTestView(t, tc.give, 40, 6) view.HandleKey(key(tcell.KeyBacktab, tcell.ModNone)) if got := view.Buffer().Text(); got != tc.want { t.Errorf("Text() = %q, want %q", got, tc.want) } }) } } func TestCutCopyAndPaste(t *testing.T) { clipboard := &Clipboard{} view, _, _ := newTestView(t, "hello world", 40, 6) view.clipboard = clipboard view.Buffer().SetCursor(buffer.Position{Line: 0, Col: 0}) view.Buffer().StartSelection() view.Buffer().SetCursorKeepingSelection(buffer.Position{Line: 0, Col: 6}) view.HandleKey(key(tcell.KeyCtrlX, tcell.ModNone)) if got := view.Buffer().Text(); got != "world" { t.Fatalf("Text() = %q after a cut, want %q", got, "world") } if got := clipboard.Text(); got != "hello " { t.Errorf("the clipboard holds %q, want %q", got, "hello ") } view.Buffer().MoveBufferEnd() view.HandleKey(key(tcell.KeyCtrlV, tcell.ModNone)) if got := view.Buffer().Text(); got != "worldhello " { t.Errorf("Text() = %q after a paste, want %q", got, "worldhello ") } } func TestTheTurboCClipboardKeysWorkToo(t *testing.T) { clipboard := &Clipboard{} view, _, _ := newTestView(t, "abc", 40, 6) view.clipboard = clipboard view.SelectAll() view.HandleKey(key(tcell.KeyInsert, tcell.ModCtrl)) // Ctrl-Ins copies if got := clipboard.Text(); got != "abc" { t.Errorf("the clipboard holds %q, want %q", got, "abc") } view.Buffer().MoveBufferEnd() view.HandleKey(key(tcell.KeyInsert, tcell.ModShift)) // Shift-Ins pastes if got := view.Buffer().Text(); got != "abcabc" { t.Errorf("Text() = %q, want %q", got, "abcabc") } view.SelectAll() view.HandleKey(key(tcell.KeyDelete, tcell.ModShift)) // Shift-Del cuts if got := view.Buffer().Text(); got != "" { t.Errorf("Text() = %q, want it emptied by the cut", got) } } func TestCopyingNothingDoesNotTouchTheClipboard(t *testing.T) { clipboard := &Clipboard{} clipboard.SetText("kept") view, _, _ := newTestView(t, "abc", 40, 6) view.clipboard = clipboard if view.Copy() || view.Cut() { t.Error("Copy or Cut reported success with nothing selected") } if got := clipboard.Text(); got != "kept" { t.Errorf("the clipboard holds %q, want it untouched", got) } } func TestPastingAnEmptyClipboardDoesNothing(t *testing.T) { view, _, _ := newTestView(t, "abc", 40, 6) if view.Paste() { t.Error("Paste reported success with an empty clipboard") } if got := view.Buffer().Text(); got != "abc" { t.Errorf("Text() = %q, want it unchanged", got) } } func TestUndoAndRedoKeys(t *testing.T) { view, _, _ := newTestView(t, "abc", 40, 6) view.Buffer().MoveBufferEnd() view.HandleKey(typed('d')) view.HandleKey(key(tcell.KeyCtrlZ, tcell.ModNone)) if got := view.Buffer().Text(); got != "abc" { t.Errorf("Text() = %q after undo, want %q", got, "abc") } // Ctrl-R, not Ctrl-Y: Ctrl-Y is Turbo C's delete-line, and that claim won. view.HandleKey(key(tcell.KeyCtrlR, tcell.ModNone)) if got := view.Buffer().Text(); got != "abcd" { t.Errorf("Text() = %q after redo, want %q", got, "abcd") } } func TestCtrlYNoLongerRedoes(t *testing.T) { // The half of the change that a user notices: the old key must not // quietly keep working, or nobody learns the new one until it bites. view, _, _ := newTestView(t, "abc", 40, 6) view.Buffer().MoveBufferEnd() view.HandleKey(typed('d')) view.HandleKey(key(tcell.KeyCtrlZ, tcell.ModNone)) view.HandleKey(key(tcell.KeyCtrlY, tcell.ModNone)) if got := view.Buffer().Text(); got == "abcd" { t.Error("Ctrl-Y redid the change; it deletes a line now") } } func TestCtrlNInsertsALineAndCtrlYDeletesOne(t *testing.T) { view, _, _ := newTestView(t, "one\ntwo\nthree\n", 40, 6) view.Buffer().SetCursor(buffer.Position{Line: 1, Col: 1}) view.HandleKey(key(tcell.KeyCtrlN, tcell.ModNone)) if got := view.Buffer().Text(); got != "one\n\ntwo\nthree\n" { t.Fatalf("after Ctrl-N Text() = %q", got) } view.HandleKey(key(tcell.KeyCtrlY, tcell.ModNone)) if got := view.Buffer().Text(); got != "one\n\nthree\n" { t.Errorf("after Ctrl-Y Text() = %q, want the line the cursor was on gone", got) } } func TestCtrlASelectsEverything(t *testing.T) { view, _, _ := newTestView(t, "one\ntwo", 40, 6) view.HandleKey(key(tcell.KeyCtrlA, tcell.ModNone)) if got := view.SelectedText(); got != "one\ntwo" { t.Errorf("SelectedText() = %q, want the whole buffer", got) } } func TestCtrlSpaceAsksForCompletion(t *testing.T) { view, _, _ := newTestView(t, "fmt.", 40, 6) asked := 0 view.OnCompletionRequest = func() { asked++ } view.HandleKey(key(tcell.KeyCtrlSpace, tcell.ModNone)) if asked != 1 { t.Errorf("completion was asked for %d times, want 1", asked) } } func TestTypingADotAsksForCompletion(t *testing.T) { view, _, _ := newTestView(t, "fmt", 40, 6) view.Buffer().MoveBufferEnd() asked := 0 view.OnCompletionRequest = func() { asked++ } view.HandleKey(typed('.')) view.HandleKey(typed('P')) if asked != 1 { t.Errorf("completion was asked for %d times, want 1 — only the dot triggers it", asked) } } func TestAKeyTheViewHasNoUseForIsPassedOn(t *testing.T) { view, _, _ := newTestView(t, "abc", 40, 6) if view.HandleKey(key(tcell.KeyF9, tcell.ModNone)) { t.Error("the view claimed a function key it does nothing with") } } func TestWordBeforeCursor(t *testing.T) { tests := []struct { name string text string col int want string }{ {"mid-identifier", "fmt.Print", 9, "Print"}, {"just after a dot", "fmt.", 4, ""}, {"at the start of a line", "abc", 0, ""}, {"a whole word", "hello", 5, "hello"}, {"with digits and underscores", "a_1b", 4, "a_1b"}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { view, _, _ := newTestView(t, tc.text, 40, 6) view.Buffer().SetCursor(buffer.Position{Line: 0, Col: tc.col}) if got := view.WordBeforeCursor(); got != tc.want { t.Errorf("WordBeforeCursor() = %q, want %q", got, tc.want) } }) } } func TestReplaceWordBeforeCursorAcceptsACompletion(t *testing.T) { view, _, _ := newTestView(t, "fmt.Prin", 40, 6) view.Buffer().MoveBufferEnd() view.ReplaceWordBeforeCursor("Println") if got := view.Buffer().Text(); got != "fmt.Println" { t.Errorf("Text() = %q, want %q", got, "fmt.Println") } } func TestGoToLine(t *testing.T) { view, _, _ := newTestView(t, strings.Repeat("x\n", 50), 40, 8) view.GoToLine(30) if got := view.Buffer().Cursor().Line; got != 29 { t.Errorf("Cursor().Line = %d, want 29 — GoToLine counts from one", got) } if top := view.TopLine(); 29 < top || 29 >= top+view.VisibleLines() { t.Error("the view did not scroll to the requested line") } } func TestClickingPlacesTheCursor(t *testing.T) { view, _, _ := newTestView(t, "package main\nfunc main() {}", 40, 8) gutter := view.gutterWidth() view.HandleMouse(tcell.NewEventMouse(gutter+4, 1, tcell.Button1, tcell.ModNone)) if got := view.Buffer().Cursor(); got != (buffer.Position{Line: 1, Col: 4}) { t.Errorf("Cursor() = %+v, want {Line:1 Col:4}", got) } } func TestDraggingSelects(t *testing.T) { view, _, _ := newTestView(t, "hello world", 40, 8) gutter := view.gutterWidth() view.HandleMouse(tcell.NewEventMouse(gutter, 0, tcell.Button1, tcell.ModNone)) view.HandleMouse(tcell.NewEventMouse(gutter+5, 0, tcell.Button1, tcell.ModNone)) if got := view.SelectedText(); got != "hello" { t.Errorf("SelectedText() = %q, want %q", got, "hello") } view.HandleMouse(tcell.NewEventMouse(gutter+5, 0, tcell.ButtonNone, tcell.ModNone)) if view.selecting { t.Error("the drag did not end when the button came back up") } } func TestTheWheelScrolls(t *testing.T) { view, _, _ := newTestView(t, strings.Repeat("line\n", 100), 40, 10) view.HandleMouse(tcell.NewEventMouse(5, 5, tcell.WheelDown, tcell.ModNone)) if got := view.TopLine(); got != wheelStep { t.Errorf("TopLine() = %d, want %d", got, wheelStep) } view.HandleMouse(tcell.NewEventMouse(5, 5, tcell.WheelUp, tcell.ModNone)) if got := view.TopLine(); got != 0 { t.Errorf("TopLine() = %d, want 0", got) } } func TestAMouseEventTheViewHasNoUseForIsPassedOn(t *testing.T) { view, _, _ := newTestView(t, "abc", 40, 6) if view.HandleMouse(tcell.NewEventMouse(1, 1, tcell.Button3, tcell.ModNone)) { t.Error("the view claimed a middle-button event it does nothing with") } } func TestTheCursorIsReportedWhenItMoves(t *testing.T) { view, _, _ := newTestView(t, "abc", 40, 6) moves := 0 view.OnCursorMove = func() { moves++ } view.HandleKey(key(tcell.KeyRight, tcell.ModNone)) view.HandleKey(typed('x')) // an edit moves the cursor too if moves != 2 { t.Errorf("OnCursorMove was called %d times, want 2", moves) } }