package acp_test import ( "strings" "testing" "github.com/gdamore/tcell/v2" "rickub.com/turbo-editors/turbo-core/acp" "rickub.com/turbo-editors/turbo-core/theme" "rickub.com/turbo-editors/turbo-core/ui" ) // drawWithCursor paints a focused view and returns the rows and where the // terminal's cursor was put — or -1, -1 when it was hidden, which is what // happened to a cursor that ran off the edge of the box. func drawWithCursor(t *testing.T, view *acp.View, width, height int) (rows []string, x, y int) { t.Helper() screen := tcell.NewSimulationScreen("UTF-8") if err := screen.Init(); err != nil { t.Fatalf("starting the screen: %v", err) } defer screen.Fini() screen.SetSize(width, height) th, err := theme.Load(theme.DefaultName, "") if err != nil { t.Fatalf("loading the theme: %v", err) } view.SetFocused(true) view.Draw(ui.NewPainter(screen), th) screen.Show() cells, w, h := screen.GetContents() rows = make([]string, h) for row := range h { var text strings.Builder for col := range w { text.WriteString(string(cells[row*w+col].Runes)) } rows[row] = strings.TrimRight(text.String(), " ") } x, y, visible := screen.GetCursor() if !visible { return rows, -1, -1 } return rows, x, y } // inputRows returns the rows of the box you type in: everything below the // rule. func inputRows(rows []string) []string { for i, row := range rows { if strings.Contains(row, "─") { return rows[i+1:] } } return nil } func typeInto(view *acp.View, text string) { for _, r := range text { view.HandleKey(tcell.NewEventKey(tcell.KeyRune, r, tcell.ModNone)) } } func TestALongPromptWrapsAtSpacesInsideTheBox(t *testing.T) { // This is the feature: before, the line was cut off with an ellipsis at // the edge and the rest of what you typed was invisible. view, _ := newView(t, 30, 12) typeInto(view, "please explain how the wrapping of a long prompt works") rows, _, _ := drawWithCursor(t, view, 30, 12) box := inputRows(rows) if !strings.HasPrefix(box[0], "> please explain how the") || box[1] != " wrapping of a long prompt" || box[2] != " works" { t.Errorf("the box is not wrapped at spaces:\n%s", strings.Join(box, "\n")) } if strings.Contains(strings.Join(box, "\n"), "…") { t.Errorf("the box still cuts the prompt off:\n%s", strings.Join(box, "\n")) } } func TestTheCursorFollowsTheTextOntoTheWrappedRow(t *testing.T) { view, _ := newView(t, 30, 12) typeInto(view, "please explain how the wrapping") _, x, y := drawWithCursor(t, view, 30, 12) // Row 0 of the box is screen row 9 (12 rows: 8 of conversation, the rule, // then 3 of box); "wrapping" is 8 runes on the second row, after "> " and // its own indent. if x != 2+len("wrapping") || y != 10 { t.Errorf("cursor at (%d, %d), want it after %q on the second row (%d, %d)", x, y, "wrapping", 2+len("wrapping"), 10) } } func TestTheCursorAtTheEndOfAFullRowStaysInsideThePane(t *testing.T) { // A row holds width-3 runes: prompt, space, and a column for exactly // this cursor. Without it the cursor sat on the frame and tcell hid it. view, _ := newView(t, 30, 12) typeInto(view, strings.Repeat("x", 27)) _, x, y := drawWithCursor(t, view, 30, 12) if x != 29 || y != 9 { t.Errorf("cursor at (%d, %d), want (29, 9): the last column of the pane, first row of the box", x, y) } } func TestAWordLongerThanTheBoxBreaksAtTheEdgeRatherThanVanishing(t *testing.T) { view, _ := newView(t, 30, 12) typeInto(view, strings.Repeat("a", 40)) rows, _, _ := drawWithCursor(t, view, 30, 12) box := inputRows(rows) if box[0] != "> "+strings.Repeat("a", 27) || box[1] != " "+strings.Repeat("a", 13) { t.Errorf("a long word is not broken at the edge:\n%s", strings.Join(box, "\n")) } } func TestUpAndDownMoveByRowInsideAWrappedPrompt(t *testing.T) { // Three rows to the eye are three rows to the arrow keys. Up from the last // row lands on the middle one, not on the conversation. view, _ := newView(t, 30, 12) typeInto(view, "please explain how the wrapping of a long prompt works") if !view.HandleKey(tcell.NewEventKey(tcell.KeyUp, 0, tcell.ModNone)) { t.Fatal("Up was not taken by the box, though there is a row above the cursor") } _, x, y := drawWithCursor(t, view, 30, 12) if y != 10 || x != 2+len("works") { t.Errorf("after Up the cursor is at (%d, %d), want the middle row at the same column (%d, 10)", x, y, 2+len("works")) } view.HandleKey(tcell.NewEventKey(tcell.KeyUp, 0, tcell.ModNone)) view.HandleKey(tcell.NewEventKey(tcell.KeyDown, 0, tcell.ModNone)) _, x, y = drawWithCursor(t, view, 30, 12) if y != 10 || x != 2+len("works") { t.Errorf("Up then Down did not come back: cursor at (%d, %d)", x, y) } // The column is kept, not the word: five in on the middle row is between // "wrapp" and "ing", which is where a key pressed now goes. typeInto(view, "!") if got := view.Input(); got != "please explain how the wrapp!ing of a long prompt works" { t.Errorf("typing after the moves went to the wrong place: %q", got) } } func TestUpFromTheFirstRowOfTheBoxIsLeftForTheConversation(t *testing.T) { view, _ := newView(t, 30, 12) typeInto(view, "short") if view.HandleKey(tcell.NewEventKey(tcell.KeyUp, 0, tcell.ModNone)) && view.Input() != "short" { t.Error("Up on a one-row prompt changed the input") } } func TestAPromptTallerThanTheBoxScrollsToKeepTheCursorInSight(t *testing.T) { // Three rows of box, five rows of prompt: the first two go up under the // rule, and the prompt marker goes with them. view, _ := newView(t, 30, 12) typeInto(view, "one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen twenty") rows, _, y := drawWithCursor(t, view, 30, 12) box := inputRows(rows) if y != 11 { t.Errorf("the cursor is on screen row %d, want the last row of the box, 11", y) } if strings.HasPrefix(box[0], ">") { t.Errorf("the prompt marker is beside a row that is not the first:\n%s", strings.Join(box, "\n")) } if !strings.HasSuffix(box[2], "twenty") { t.Errorf("the last row does not end with what was typed last:\n%s", strings.Join(box, "\n")) } } func TestATypedTrailingSpaceAtTheEdgePutsTheCursorOnTheNextRow(t *testing.T) { // 27 runes fill a row; the space after them is the break, not drawn, and // the cursor is at the start of an empty row below rather than on the frame. view, _ := newView(t, 30, 12) typeInto(view, strings.Repeat("x", 27)+" ") _, x, y := drawWithCursor(t, view, 30, 12) if x != 2 || y != 10 { t.Errorf("cursor at (%d, %d), want (2, 10)", x, y) } }