From a07d4fb802f3cef327e2ba50d4f8a6beb7c8859b Mon Sep 17 00:00:00 2001 From: Artem Lytkin Date: Fri, 25 Sep 2026 22:06:17 +0300 Subject: [PATCH] action: Don't re-wrap the rest of the buffer on PageDown with softwrap ScrollAdjust(), ScrollReachedEnd(), GotoLoc() and initialRelocate() use Diff() only to compare a distance with the view height. With softwrap on, Diff() counts the rows of every line in between, so PageDown near the top of a large file wraps everything down to the end of the buffer, and GotoLoc() wraps everything between the old and the new location. Compare against Scroll() by at most a view height instead, the same way Relocate() and CursorToViewBottom() already do. Scroll() stops after that many rows, so the cost no longer depends on the buffer size, and the resulting view is the same. Fixes #3964 --- cmd/micro/micro_test.go | 58 ++++++++++++++++++++++++++++++++++++++ internal/action/actions.go | 6 ++-- internal/action/bufpane.go | 6 ++-- 3 files changed, 64 insertions(+), 6 deletions(-) diff --git a/cmd/micro/micro_test.go b/cmd/micro/micro_test.go index 185b296e5b..f00622d2b7 100644 --- a/cmd/micro/micro_test.go +++ b/cmd/micro/micro_test.go @@ -4,12 +4,14 @@ import ( "fmt" "log" "os" + "strings" "testing" "github.com/go-errors/errors" "github.com/micro-editor/micro/v2/internal/action" "github.com/micro-editor/micro/v2/internal/buffer" "github.com/micro-editor/micro/v2/internal/config" + "github.com/micro-editor/micro/v2/internal/display" "github.com/micro-editor/micro/v2/internal/screen" "github.com/micro-editor/micro/v2/internal/util" "github.com/micro-editor/tcell/v2" @@ -329,6 +331,62 @@ func TestSearchAndReplace(t *testing.T) { assert.Equal(t, srTest3, string(data)) } +func TestSoftwrapScroll(t *testing.T) { + // lines of 1, 3, 6 and 8 rows, so rows and lines don't match up + var sb strings.Builder + for i := 0; i < 50; i++ { + fmt.Fprintf(&sb, "%d %s\n", i, strings.Repeat("word ", i%4*40)) + } + text := sb.String() + openFile(createTestFile(t, text)) + h := action.MainTab().CurPane() + h.Buf.SetOptionNative("softwrap", true) + height := h.BufView().Height + end := h.SLocFromLoc(h.Buf.End()) + lastPage := h.Scroll(end, -height+1) + + // check against Diff() for every StartLine near the end + v := h.GetView() + for s := h.Scroll(end, -3*height); s != end; s = h.Scroll(s, 1) { + v.StartLine = s + assert.Equal(t, h.Diff(s, end) < height, h.ScrollReachedEnd(), "ScrollReachedEnd at %v", s) + want := s + if h.Diff(s, end) < height-1 { + want = lastPage + } + h.ScrollAdjust() + assert.Equal(t, want, h.GetView().StartLine, "ScrollAdjust at %v", s) + } + + // PageDown stops with the last line at the bottom of the view + h.GotoLoc(buffer.Loc{X: 0, Y: 0}) + for i := 0; i < 50; i++ { + h.PageDown() + } + assert.Equal(t, lastPage, h.GetView().StartLine) + + // GotoLoc recenters only when the cursor moves by at least a view height + from := h.Scroll(end, -2*height) + for _, d := range []int{height - 1, height, 1 - height, -height} { + h.GotoLoc(h.LocFromVLoc(display.VLoc{SLoc: from})) + sloc := h.Scroll(from, d) + h.GotoLoc(h.LocFromVLoc(display.VLoc{SLoc: sloc})) + recentered := h.GetView().StartLine == h.Scroll(sloc, -height/4) + assert.Equal(t, util.Abs(d) >= height, recentered, "GotoLoc by %d rows", d) + } + + // opening a buffer recenters only when the cursor is not on the first page + for _, d := range []int{height - 1, height} { + sloc := h.Scroll(display.SLoc{}, d) + b := buffer.NewBufferFromString(text, "", buffer.BTDefault) + b.SetOptionNative("softwrap", true) + b.GetActiveCursor().GotoLoc(h.LocFromVLoc(display.VLoc{SLoc: sloc})) + h.OpenBuffer(b) + recentered := h.GetView().StartLine == h.Scroll(sloc, -height/4) + assert.Equal(t, d >= height, recentered, "open at row %d", d) + } +} + func TestMultiCursor(t *testing.T) { // TODO } diff --git a/internal/action/actions.go b/internal/action/actions.go index ce1d0e1099..f1ec01f619 100644 --- a/internal/action/actions.go +++ b/internal/action/actions.go @@ -40,8 +40,8 @@ func (h *BufPane) ScrollDown(n int) { func (h *BufPane) ScrollAdjust() { v := h.GetView() end := h.SLocFromLoc(h.Buf.End()) - if h.Diff(v.StartLine, end) < h.BufView().Height-1 { - v.StartLine = h.Scroll(end, -h.BufView().Height+1) + if last := h.Scroll(end, -h.BufView().Height+1); last.LessThan(v.StartLine) { + v.StartLine = last } h.SetView(v) } @@ -51,7 +51,7 @@ func (h *BufPane) ScrollAdjust() { func (h *BufPane) ScrollReachedEnd() bool { v := h.GetView() end := h.SLocFromLoc(h.Buf.End()) - return h.Diff(v.StartLine, end) < h.BufView().Height + return end.LessEqual(h.Scroll(v.StartLine, h.BufView().Height-1)) } // MousePress is the event that should happen when a normal click happens diff --git a/internal/action/bufpane.go b/internal/action/bufpane.go index df6a20f8d8..939eb8ed2f 100644 --- a/internal/action/bufpane.go +++ b/internal/action/bufpane.go @@ -361,14 +361,14 @@ func (h *BufPane) OpenBuffer(b *buffer.Buffer) { // Use GotoLoc when the new location may be far away from the current location. func (h *BufPane) GotoLoc(loc buffer.Loc) { sloc := h.SLocFromLoc(loc) - d := h.Diff(h.SLocFromLoc(h.Cursor.Loc), sloc) + cur := h.SLocFromLoc(h.Cursor.Loc) h.Cursor.GotoLoc(loc) // If the new location is far away from the previous one, // ensure the cursor is at 25% of the window height height := h.BufView().Height - if util.Abs(d) >= height { + if sloc.GreaterThan(h.Scroll(cur, height-1)) || sloc.LessThan(h.Scroll(cur, -height+1)) { v := h.GetView() v.StartLine = h.Scroll(sloc, -height/4) h.ScrollAdjust() @@ -384,7 +384,7 @@ func (h *BufPane) initialRelocate() { // If the initial cursor location is far away from the beginning // of the buffer, ensure the cursor is at 25% of the window height v := h.GetView() - if h.Diff(display.SLoc{0, 0}, sloc) < height { + if sloc.LessEqual(h.Scroll(display.SLoc{0, 0}, height-1)) { v.StartLine = display.SLoc{0, 0} } else { v.StartLine = h.Scroll(sloc, -height/4)