Don't leave a re-rendered diff scrolled further right than it reaches

The main view keeps its horizontal scroll position when a diff is
rendered again. If the new rendering is narrower, for example because
the only long line in it was just staged, the view can end up scrolled
past the end of every line and look empty. If one line still reaches
into the view, the view shows only a sliver of it, although the whole
diff would fit without scrolling.

Once a render has been read to the end, scroll the view back as far as
it takes to put the end of the widest line at the right edge. A long
diff that is read only in part is checked once the rest of it has been
read.

Nothing scrolls the main view horizontally yet, so this changes nothing
on its own. It prepares for scrolling the focused main view sideways
with H and L.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Stefan Haller
2026-10-10 13:44:43 +02:00
co-authored by Claude Opus 5.5
parent 4f678b7d0d
commit 6da4f658d7
3 changed files with 62 additions and 0 deletions
+31
View File
@@ -2693,6 +2693,37 @@ func (v *View) ScrollRight(amount int) {
v.clearHover()
}
// ClampOriginXToContent scrolls the view back to the left if it is scrolled further
// than its content needs, so that the end of the widest line is at the right edge.
// A view that doesn't wrap keeps its horizontal scroll position when its content
// changes, and the new content may not reach as far.
func (v *View) ClampOriginXToContent() {
v.writeMutex.Lock()
defer v.writeMutex.Unlock()
if v.ox == 0 {
return
}
v.refreshViewLinesIfNeeded()
widest := 0
for _, vline := range v.viewLines {
width := 0
for _, c := range vline.line {
width += c.width
}
widest = max(widest, width)
}
contentWidth := v.InnerWidth() - v.inclusionGutterWidth()
newOx := min(v.ox, max(0, widest-contentWidth))
if newOx != v.ox {
v.SetOriginX(newOx)
v.clearHover()
}
}
func (v *View) adjustDownwardScrollAmount(scrollHeight int) int {
_, oy := v.Origin()
y := oy
+30
View File
@@ -1351,6 +1351,36 @@ func TestInclusionGutterWithHorizontallyScrolledContent(t *testing.T) {
assert.Equal(t, " 3456789a", row(2))
}
// A view scrolled further right than its content needs goes back as far as it takes
// to put the end of the widest line at the right edge, and no further.
func TestClampOriginXToContent(t *testing.T) {
v := NewView("name", 0, 0, 11, 5, OutputNormal) // InnerWidth 10
v.InclusionGutterMarker = "✓"
// The widest line is 16 wide.
v.writeString("0123456789abcdef\nshort\n")
v.SetOriginX(4)
v.ClampOriginXToContent()
assert.Equal(t, 4, v.OriginX(), "the widest line still reaches the right edge")
v.SetOriginX(10)
v.ClampOriginXToContent()
assert.Equal(t, 6, v.OriginX())
// The gutter leaves the content eight columns.
v.SetInclusionGutter(true, []bool{false, false})
v.SetOriginX(10)
v.ClampOriginXToContent()
assert.Equal(t, 8, v.OriginX())
// Content that fits needs no scrolling at all.
v.Clear()
v.writeString("short\n")
v.ClampOriginXToContent()
assert.Equal(t, 0, v.OriginX())
}
// A marked line the view wraps is marked on every segment it is drawn as, so that
// the mark doesn't look like it belongs to the first part of the line alone. The
// gutter takes its columns out of the width the content wraps in.
+1
View File
@@ -235,6 +235,7 @@ func (gui *Gui) getManager(view *gocui.View) *tasks.ViewBufferManager {
view.SetOriginY(newOriginY)
}
view.ClampOriginXToContent()
gui.updateDiffPaneDecorations(view, true)
gui.clampDiffSelectionToContent(view)