Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions internal/display/hint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ func rowIndex(screen, substr string) int {
return -1
}

func compactScreen(screen string) string {
return strings.Join(strings.Fields(screen), "")
}

// TestHintProviderTracksLine checks that a registered passive hint provider is
// re-evaluated as the input changes: the provided lane echoes the current line.
func TestHintProviderTracksLine(t *testing.T) {
Expand Down
17 changes: 15 additions & 2 deletions internal/display/refresh.go
Original file line number Diff line number Diff line change
Expand Up @@ -271,11 +271,24 @@ func (e *Engine) displayLineRefactored() {
if len(e.suggested) > e.line.Len() && e.opts.GetBool("history-autosuggest") {
line += color.Dim + color.Fmt(color.Fg+"242") + string(e.suggested[e.line.Len():]) + color.Reset
}
// Format tabs as spaces, for consistent display
line = strutil.FormatTabs(line) + term.ClearLineAfter
// Format tabs as spaces, for consistent display. When the rendered input
// lands exactly on the terminal edge, there is no current-row tail to clear;
// writing EL in the terminal's pending-wrap state can erase the edge glyph.
wrappedAtRightEdge := e.lineCol == 0 && len(line) > 0
line = strutil.FormatTabs(line)
if !wrappedAtRightEdge {
line += term.ClearLineAfter
}
// And display the line.
e.suggested.Set([]rune(line)...)
core.DisplayLine(&e.suggested, e.startCols)

// If the rendered input lands exactly on the terminal edge, terminals keep
// the cursor in a pending-wrap state. Force the wrap before later clear and
// cursor movement sequences so redraws do not overwrite or scroll input.
if wrappedAtRightEdge {
term.WriteString(term.NewlineReturn)
}
}

func (e *Engine) renderMultilineIndicators() {
Expand Down
48 changes: 48 additions & 0 deletions internal/display/render_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,54 @@ func TestRenderWrappingAlignment(t *testing.T) {
}
}

// TestRenderRightEdgeWrapAtBottomKeepsInputVisiblePerKey guards the common
// shell position: the prompt is already at the bottom of the terminal, and the
// typed input soft-wraps at the right edge. Each keypress should keep the
// current input visible instead of erasing the edge character or scrolling one
// row per refresh.
func TestRenderRightEdgeWrapAtBottomKeepsInputVisiblePerKey(t *testing.T) {
const (
cols = 20
rows = 8
)
const prompt = "P> "

c := startConsole(t, consoleConfig{
prompt: prompt,
cols: cols,
rows: rows,
prefill: rows - 1,
})

typed := "abcdefghijklmnopqrstu"
firstWrappedInputLen := cols - len(prompt) + 1
stableTopRow := -1

c.waitForScreen(prompt)
for idx, ch := range typed {
c.send(string(ch))
want := typed[:idx+1]

screen := c.waitUntil(func(screen string) bool {
return strings.Contains(compactScreen(screen), "P>"+want)
})

if idx+1 == firstWrappedInputLen {
stableTopRow = rowIndex(screen, prompt)
}
if idx+1 > firstWrappedInputLen {
if got := rowIndex(screen, prompt); got != stableTopRow {
t.Fatalf("prompt scrolled while typing within the same wrapped row after key %d (%q): prompt row %d, want %d\n%s", idx+1, ch, got, stableTopRow, screen)
}
}
}

c.send("\r")
c.waitUntil(func(screen string) bool {
return strings.Contains(compactScreen(screen), "[LINE:"+typed+"]")
})
}

// TestRenderMultilinePromptAtBottom guards the bottom-of-window case: a
// multi-line primary prompt rendered on the last row of the terminal must keep
// all of its lines (including the input line) instead of overlapping them.
Expand Down