Fix cursorUp offset in incremental rendering for trailing newline#910
Conversation
Made-with: Cursor
|
I think this needs a small regression test before merging. The fix itself looks right, but the current tests still don't pin down the case that broke here: a same-height incremental update after a render that ends with a trailing newline. Right now the nearby tests only check that the update contains some cursor movement and the changed line text, so this exact off-by-one could come back later and still pass. I'd add one focused assertion around a flow like |
Made-with: Cursor
|
Thanks for the review! Added a focused regression test in 389a241 that asserts the exact render('Line 1\nLine 2\nLine 3\n');
render('Line 1\nUpdated\nLine 3\n');
// Asserts secondCall starts with cursorUp(3), not cursorUp(2)
t.true(secondCall.startsWith(ansiEscapes.cursorUp(3)));The test verifies the second write rewinds all the way to row 0 ( |
Summary
Fixes the cursor rewind offset in
createIncremental(src/log-update.ts) that causes ghost/duplicate lines when the rendered output has a trailing newline.Root cause: The "same or grow height" branch uses
cursorUp(previousVisible - 1)to rewind to line 0.previousVisibleisvisibleLineCount()which excludes the trailing empty line fromsplit('\n'). But after writing output ending with'\n', the cursor sits one row past the last visible line. So the rewind falls one row short — line 0 is never reached, all writes land one row too low, and the old line 0 content remains as a ghost line.Fix: Replace
previousVisible - 1withpreviousLines.length - 1. This correctly accounts for the cursor position in both cases:'\n'(non-fullscreen):previousLines.length = previousVisible + 1, solength - 1 = previousVisible→ rewinds to row 0 ✅'\n'(fullscreen):previousLines.length = previousVisible, solength - 1 = previousVisible - 1→ same as before ✅The "shrink" branch is unaffected because it already handles trailing newlines via the
extraSlotvariable.Changes
One line in
src/log-update.ts:} else { - buffer.push(ansiEscapes.cursorUp(previousVisible - 1)); + buffer.push(ansiEscapes.cursorUp(previousLines.length - 1)); }Test Plan
incrementalRendering: truethat state changes update lines in place without ghost linestest/log-update.tsx: 40 tests,test/cursor-helpers.tsx: 17 tests)Related
Made with Cursor