Skip to content

Commit d3aedcb

Browse files
authored
Merge pull request #117 from reeflective/dev
fix(display): unify line renderer and restore right-edge wrap handling
2 parents 19f6714 + ef40244 commit d3aedcb

4 files changed

Lines changed: 82 additions & 51 deletions

File tree

internal/display/engine.go

Lines changed: 0 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,9 @@ import (
55
"strings"
66

77
"github.com/reeflective/readline/inputrc"
8-
"github.com/reeflective/readline/internal/color"
98
"github.com/reeflective/readline/internal/completion"
109
"github.com/reeflective/readline/internal/core"
1110
"github.com/reeflective/readline/internal/history"
12-
"github.com/reeflective/readline/internal/strutil"
1311
"github.com/reeflective/readline/internal/term"
1412
"github.com/reeflective/readline/internal/ui"
1513
)
@@ -249,51 +247,6 @@ func (e *Engine) computeCoordinates(suggested bool) {
249247
e.primaryPrinted = false
250248
}
251249

252-
func (e *Engine) displayLine() {
253-
var line string
254-
255-
// Apply user-defined highlighter to the input line.
256-
if e.highlighter != nil {
257-
line = e.highlighter(*e.line)
258-
} else {
259-
line = string(*e.line)
260-
}
261-
262-
// Highlight matching parenthesis
263-
if e.opts.GetBool("blink-matching-paren") {
264-
core.HighlightMatchers(e.selection)
265-
defer core.ResetMatchers(e.selection)
266-
}
267-
268-
// Apply visual selections highlighting if any
269-
line = e.highlightLine([]rune(line), *e.selection)
270-
271-
// Get the subset of the suggested line to print.
272-
suggestionAdded := false
273-
if len(e.suggested) > e.line.Len() && e.opts.GetBool("history-autosuggest") {
274-
line += color.Dim + color.Fmt(color.Fg+"242") + string(e.suggested[e.line.Len():]) + color.Reset
275-
suggestionAdded = true
276-
}
277-
278-
currentLine := string(*e.line)
279-
if !suggestionAdded && e.inlineSuggestionApplies(currentLine) {
280-
line += color.Dim + color.Fmt(color.Fg+"242") + e.inline[len(currentLine):] + color.Reset
281-
}
282-
283-
// Format tabs as spaces, for consistent display
284-
line = strutil.FormatTabs(line) + term.ClearLineAfter
285-
286-
// And display the line.
287-
e.suggested.Set([]rune(line)...)
288-
core.DisplayLine(&e.suggested, e.startCols)
289-
290-
// Adjust the cursor if the line fits exactly in the terminal width.
291-
if e.lineCol == 0 {
292-
term.WriteString(term.NewlineReturn)
293-
term.WriteString(term.ClearLineAfter)
294-
}
295-
}
296-
297250
// AvailableHelperLines returns the number of lines available below the hint section.
298251
// It returns half the terminal space if we currently have less than 1/3rd of it below.
299252
func (e *Engine) AvailableHelperLines() int {

internal/display/hint_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ import (
77
"testing"
88
)
99

10+
// compactScreen collapses all runs of whitespace so screen contents can be
11+
// matched irrespective of terminal padding or soft-wrap column boundaries.
12+
func compactScreen(screen string) string {
13+
return strings.Join(strings.Fields(screen), "")
14+
}
15+
1016
// rowIndex returns the index of the first screen row containing substr, or -1.
1117
func rowIndex(screen, substr string) int {
1218
for i, row := range strings.Split(screen, "\n") {

internal/display/refresh.go

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ func (e *Engine) repaintPromptUpperLines() {
114114
}
115115

116116
func (e *Engine) renderInputArea() {
117-
e.displayLineRefactored()
117+
e.displayLine()
118118
e.renderMultilineIndicators()
119119
e.renderRightPrompt()
120120
}
@@ -252,7 +252,11 @@ func (e *Engine) ensureInputSpace() {
252252
term.MoveCursorForwards(e.startCols)
253253
}
254254

255-
func (e *Engine) displayLineRefactored() {
255+
// displayLine renders the input line — highlighting, history-autosuggest and
256+
// inline-suggestion suffixes, and tab expansion — at the current cursor row. It
257+
// is the single line renderer shared by the main refresh path (renderInputArea)
258+
// and the transient-prompt redraw (RefreshTransient).
259+
func (e *Engine) displayLine() {
256260
var line string
257261
// Apply user-defined highlighter to the input line.
258262
if e.highlighter != nil {
@@ -278,11 +282,26 @@ func (e *Engine) displayLineRefactored() {
278282
if !suggestionAdded && e.inlineSuggestionApplies(currentLine) {
279283
line += color.Dim + color.Fmt(color.Fg+"242") + e.inline[len(currentLine):] + color.Reset
280284
}
281-
// Format tabs as spaces, for consistent display
282-
line = strutil.FormatTabs(line) + term.ClearLineAfter
285+
286+
// Format tabs as spaces, for consistent display. When the rendered input
287+
// lands exactly on the terminal's right edge (lineCol == 0), the cursor is
288+
// left in the terminal's pending-wrap state; emitting clear-to-end-of-line
289+
// there can erase the edge glyph, so skip it in that case.
290+
wrappedAtRightEdge := e.lineCol == 0 && len(line) > 0
291+
line = strutil.FormatTabs(line)
292+
if !wrappedAtRightEdge {
293+
line += term.ClearLineAfter
294+
}
295+
283296
// And display the line.
284297
e.suggested.Set([]rune(line)...)
285298
core.DisplayLine(&e.suggested, e.startCols)
299+
300+
// Force the pending wrap before any later clear/cursor-movement sequences,
301+
// so redraws do not overwrite the edge character or scroll one row per key.
302+
if wrappedAtRightEdge {
303+
term.WriteString(term.NewlineReturn)
304+
}
286305
}
287306

288307
func (e *Engine) renderMultilineIndicators() {

internal/display/render_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,3 +192,56 @@ func TestRenderWithCursorProbeDisabled(t *testing.T) {
192192
t.Fatalf("row 0 misaligned with probing disabled:\n got: %q\n want: %q", row0, want)
193193
}
194194
}
195+
196+
// TestRenderRightEdgeWrapAtBottomKeepsInputVisiblePerKey guards the common
197+
// shell position: the prompt is already at the bottom of the terminal, and the
198+
// typed input soft-wraps at the right edge. Each keypress should keep the
199+
// current input visible instead of erasing the edge character or scrolling one
200+
// row per refresh.
201+
//
202+
// This is a regression test for the right-edge redraw fix (originally proposed
203+
// in PR #112 by @rztaylor): the display refactor dropped the pending-wrap
204+
// handling from the main render path, so typing past the right edge on the last
205+
// row wedged the redraw.
206+
func TestRenderRightEdgeWrapAtBottomKeepsInputVisiblePerKey(t *testing.T) {
207+
const (
208+
cols = 20
209+
rows = 8
210+
)
211+
const prompt = "P> "
212+
213+
c := startConsole(t, consoleConfig{
214+
prompt: prompt,
215+
cols: cols,
216+
rows: rows,
217+
prefill: rows - 1,
218+
})
219+
220+
typed := "abcdefghijklmnopqrstu"
221+
firstWrappedInputLen := cols - len(prompt) + 1
222+
stableTopRow := -1
223+
224+
c.waitForScreen(prompt)
225+
for idx, ch := range typed {
226+
c.send(string(ch))
227+
want := typed[:idx+1]
228+
229+
screen := c.waitUntil(func(screen string) bool {
230+
return strings.Contains(compactScreen(screen), "P>"+want)
231+
})
232+
233+
if idx+1 == firstWrappedInputLen {
234+
stableTopRow = rowIndex(screen, prompt)
235+
}
236+
if idx+1 > firstWrappedInputLen {
237+
if got := rowIndex(screen, prompt); got != stableTopRow {
238+
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)
239+
}
240+
}
241+
}
242+
243+
c.send("\r")
244+
c.waitUntil(func(screen string) bool {
245+
return strings.Contains(compactScreen(screen), "[LINE:"+typed+"]")
246+
})
247+
}

0 commit comments

Comments
 (0)