]> git.lizzy.rs Git - micro.git/commitdiff
Fix softwrap scrolling issues (#1981)
authorDmitry Maluka <dmitrymaluka@gmail.com>
Wed, 7 Apr 2021 20:18:51 +0000 (22:18 +0200)
committerGitHub <noreply@github.com>
Wed, 7 Apr 2021 20:18:51 +0000 (16:18 -0400)
Softwrap implementation enhanced to fix various issues with scrolling,
centering, relocating etc.

The main idea is simple: work not with simple line numbers but
with (Line, Row) pairs, where Line is a line number in the buffer
and Row is a visual line (a row) number within this line.
The logic remains mostly the same, but simple arithmetic operations
on line numbers are replaced with corresponding operations on
(Line, Row) pairs.

Fixes #632, #1657

cmd/micro/initlua.go
internal/action/actions.go
internal/action/globals.go
internal/display/bufwindow.go
internal/display/infowindow.go
internal/display/softwrap.go [new file with mode: 0644]
internal/display/window.go
runtime/help/plugins.md

index 2ed94c3d0f76491c42f0ac6ec60406537e6e1f5c..dcb608309f3f7dde26bc21df59c0ed3f006cbfdf 100644 (file)
@@ -118,6 +118,9 @@ func luaImportMicroBuffer() *lua.LTable {
        ulua.L.SetField(pkg, "Loc", luar.New(ulua.L, func(x, y int) buffer.Loc {
                return buffer.Loc{x, y}
        }))
+       ulua.L.SetField(pkg, "SLoc", luar.New(ulua.L, func(line, row int) display.SLoc {
+               return display.SLoc{line, row}
+       }))
        ulua.L.SetField(pkg, "BTDefault", luar.New(ulua.L, buffer.BTDefault.Kind))
        ulua.L.SetField(pkg, "BTHelp", luar.New(ulua.L, buffer.BTHelp.Kind))
        ulua.L.SetField(pkg, "BTLog", luar.New(ulua.L, buffer.BTLog.Kind))
index 44eadbf83cb74806d2424d4f38e41a24c4bfb6fe..6db8e82203e97c294de4f9cf671a819b775f36bc 100644 (file)
@@ -10,6 +10,7 @@ import (
        "github.com/zyedidia/micro/v2/internal/buffer"
        "github.com/zyedidia/micro/v2/internal/clipboard"
        "github.com/zyedidia/micro/v2/internal/config"
+       "github.com/zyedidia/micro/v2/internal/display"
        "github.com/zyedidia/micro/v2/internal/screen"
        "github.com/zyedidia/micro/v2/internal/shell"
        "github.com/zyedidia/micro/v2/internal/util"
@@ -19,21 +20,26 @@ import (
 // ScrollUp is not an action
 func (h *BufPane) ScrollUp(n int) {
        v := h.GetView()
-       if v.StartLine >= n {
-               v.StartLine -= n
-               h.SetView(v)
-       } else {
-               v.StartLine = 0
-       }
+       v.StartLine = h.Scroll(v.StartLine, -n)
+       h.SetView(v)
 }
 
 // ScrollDown is not an action
 func (h *BufPane) ScrollDown(n int) {
        v := h.GetView()
-       if v.StartLine <= h.Buf.LinesNum()-1-n {
-               v.StartLine += n
-               h.SetView(v)
+       v.StartLine = h.Scroll(v.StartLine, n)
+       h.SetView(v)
+}
+
+// If the user has scrolled past the last line, ScrollAdjust can be used
+// to shift the view so that the last line is at the bottom
+func (h *BufPane) ScrollAdjust() {
+       v := h.GetView()
+       end := h.SLocFromLoc(h.Buf.End())
+       if h.Diff(v.StartLine, end) < v.Height-1 {
+               v.StartLine = h.Scroll(end, -v.Height+1)
        }
+       h.SetView(v)
 }
 
 // MousePress is the event that should happen when a normal click happens
@@ -111,15 +117,9 @@ func (h *BufPane) ScrollDownAction() bool {
 // Center centers the view on the cursor
 func (h *BufPane) Center() bool {
        v := h.GetView()
-       v.StartLine = h.Cursor.Y - v.Height/2
-       if v.StartLine+v.Height > h.Buf.LinesNum() {
-               v.StartLine = h.Buf.LinesNum() - v.Height
-       }
-       if v.StartLine < 0 {
-               v.StartLine = 0
-       }
+       v.StartLine = h.Scroll(h.SLocFromLoc(h.Cursor.Loc), -v.Height/2)
        h.SetView(v)
-       h.Relocate()
+       h.ScrollAdjust()
        return true
 }
 
@@ -1243,45 +1243,31 @@ func (h *BufPane) JumpLine() bool {
 // Start moves the viewport to the start of the buffer
 func (h *BufPane) Start() bool {
        v := h.GetView()
-       v.StartLine = 0
+       v.StartLine = display.SLoc{0, 0}
        h.SetView(v)
        return true
 }
 
 // End moves the viewport to the end of the buffer
 func (h *BufPane) End() bool {
-       // TODO: softwrap problems?
        v := h.GetView()
-       if v.Height > h.Buf.LinesNum() {
-               v.StartLine = 0
-               h.SetView(v)
-       } else {
-               v.StartLine = h.Buf.LinesNum() - v.Height
-               h.SetView(v)
-       }
+       v.StartLine = h.Scroll(h.SLocFromLoc(h.Buf.End()), -v.Height+1)
+       h.SetView(v)
        return true
 }
 
 // PageUp scrolls the view up a page
 func (h *BufPane) PageUp() bool {
        v := h.GetView()
-       if v.StartLine > v.Height {
-               h.ScrollUp(v.Height)
-       } else {
-               v.StartLine = 0
-       }
-       h.SetView(v)
+       h.ScrollUp(v.Height)
        return true
 }
 
 // PageDown scrolls the view down a page
 func (h *BufPane) PageDown() bool {
        v := h.GetView()
-       if h.Buf.LinesNum()-(v.StartLine+v.Height) > v.Height {
-               h.ScrollDown(v.Height)
-       } else if h.Buf.LinesNum() >= v.Height {
-               v.StartLine = h.Buf.LinesNum() - v.Height
-       }
+       h.ScrollDown(v.Height)
+       h.ScrollAdjust()
        return true
 }
 
@@ -1338,24 +1324,15 @@ func (h *BufPane) CursorPageDown() bool {
 // HalfPageUp scrolls the view up half a page
 func (h *BufPane) HalfPageUp() bool {
        v := h.GetView()
-       if v.StartLine > v.Height/2 {
-               h.ScrollUp(v.Height / 2)
-       } else {
-               v.StartLine = 0
-       }
-       h.SetView(v)
+       h.ScrollUp(v.Height / 2)
        return true
 }
 
 // HalfPageDown scrolls the view down half a page
 func (h *BufPane) HalfPageDown() bool {
        v := h.GetView()
-       if h.Buf.LinesNum()-(v.StartLine+v.Height) > v.Height/2 {
-               h.ScrollDown(v.Height / 2)
-       } else if h.Buf.LinesNum() >= v.Height {
-               v.StartLine = h.Buf.LinesNum() - v.Height
-       }
-       h.SetView(v)
+       h.ScrollDown(v.Height / 2)
+       h.ScrollAdjust()
        return true
 }
 
index 4a3b8375a91f8f732f71066b6dd2e4946fda214b..e20f61edfcadfa48e4a449cb34da5f1108fc3f18 100644 (file)
@@ -21,13 +21,6 @@ func WriteLog(s string) {
        buffer.WriteLog(s)
        if LogBufPane != nil {
                LogBufPane.CursorEnd()
-               v := LogBufPane.GetView()
-               endY := buffer.LogBuf.End().Y
-
-               if endY > v.StartLine+v.Height {
-                       v.StartLine = buffer.LogBuf.End().Y - v.Height + 2
-                       LogBufPane.SetView(v)
-               }
        }
 }
 
@@ -37,12 +30,4 @@ func WriteLog(s string) {
 func (h *BufPane) OpenLogBuf() {
        LogBufPane = h.HSplitBuf(buffer.LogBuf)
        LogBufPane.CursorEnd()
-
-       v := LogBufPane.GetView()
-       endY := buffer.LogBuf.End().Y
-
-       if endY > v.StartLine+v.Height {
-               v.StartLine = buffer.LogBuf.End().Y - v.Height + 2
-               LogBufPane.SetView(v)
-       }
 }
index afcfa5bc129736b4ee8a063ad4075fee22a82631..2fcf0ac50c27fffb089911e3e547ddd8c232fd85 100644 (file)
@@ -106,51 +106,35 @@ func (w *BufWindow) Clear() {
        }
 }
 
-// Bottomline returns the line number of the lowest line in the view
-// You might think that this is obviously just v.StartLine + v.Height
-// but if softwrap is enabled things get complicated since one buffer
-// line can take up multiple lines in the view
-func (w *BufWindow) Bottomline() int {
-       if !w.Buf.Settings["softwrap"].(bool) {
-               h := w.StartLine + w.Height - 1
-               if w.drawStatus {
-                       h--
-               }
-               return h
-       }
-
-       l := w.LocFromVisual(buffer.Loc{0, w.Y + w.Height})
-
-       return l.Y
-}
-
 // Relocate moves the view window so that the cursor is in view
 // This is useful if the user has scrolled far away, and then starts typing
 // Returns true if the window location is moved
 func (w *BufWindow) Relocate() bool {
        b := w.Buf
-       // how many buffer lines are in the view
-       height := w.Bottomline() + 1 - w.StartLine
-       h := w.Height
+       height := w.Height
        if w.drawStatus {
-               h--
+               height--
        }
        ret := false
        activeC := w.Buf.GetActiveCursor()
-       cy := activeC.Y
        scrollmargin := int(b.Settings["scrollmargin"].(float64))
-       if cy < w.StartLine+scrollmargin && cy > scrollmargin-1 {
-               w.StartLine = cy - scrollmargin
+
+       c := w.SLocFromLoc(activeC.Loc)
+       bStart := SLoc{0, 0}
+       bEnd := w.SLocFromLoc(b.End())
+
+       if c.LessThan(w.Scroll(w.StartLine, scrollmargin)) && c.GreaterThan(w.Scroll(bStart, scrollmargin-1)) {
+               w.StartLine = w.Scroll(c, -scrollmargin)
                ret = true
-       } else if cy < w.StartLine {
-               w.StartLine = cy
+       } else if c.LessThan(w.StartLine) {
+               w.StartLine = c
                ret = true
        }
-       if cy > w.StartLine+height-1-scrollmargin && cy < b.LinesNum()-scrollmargin {
-               w.StartLine = cy - height + 1 + scrollmargin
+       if c.GreaterThan(w.Scroll(w.StartLine, height-1-scrollmargin)) && c.LessThan(w.Scroll(bEnd, -scrollmargin+1)) {
+               w.StartLine = w.Scroll(c, -height+1+scrollmargin)
                ret = true
-       } else if cy >= b.LinesNum()-scrollmargin && cy >= height {
-               w.StartLine = b.LinesNum() - height
+       } else if c.GreaterThan(w.Scroll(bEnd, -scrollmargin)) && c.GreaterThan(w.Scroll(w.StartLine, height-1)) {
+               w.StartLine = w.Scroll(bEnd, -height+1)
                ret = true
        }
 
@@ -199,11 +183,15 @@ func (w *BufWindow) LocFromVisual(svloc buffer.Loc) buffer.Loc {
        // this represents the current draw position
        // within the current window
        vloc := buffer.Loc{X: 0, Y: 0}
+       if softwrap {
+               // the start line may be partially out of the current window
+               vloc.Y = -w.StartLine.Row
+       }
 
        // this represents the current draw position in the buffer (char positions)
-       bloc := buffer.Loc{X: -1, Y: w.StartLine}
+       bloc := buffer.Loc{X: -1, Y: w.StartLine.Line}
 
-       for vloc.Y = 0; vloc.Y < bufHeight; vloc.Y++ {
+       for ; vloc.Y < bufHeight; vloc.Y++ {
                vloc.X = 0
                if hasMessage {
                        vloc.X += 2
@@ -473,14 +461,18 @@ func (w *BufWindow) displayBuffer() {
        // this represents the current draw position
        // within the current window
        vloc := buffer.Loc{X: 0, Y: 0}
+       if softwrap {
+               // the start line may be partially out of the current window
+               vloc.Y = -w.StartLine.Row
+       }
 
        // this represents the current draw position in the buffer (char positions)
-       bloc := buffer.Loc{X: -1, Y: w.StartLine}
+       bloc := buffer.Loc{X: -1, Y: w.StartLine.Line}
 
        cursors := b.GetCursors()
 
        curStyle := config.DefStyle
-       for vloc.Y = 0; vloc.Y < bufHeight; vloc.Y++ {
+       for ; vloc.Y < bufHeight; vloc.Y++ {
                vloc.X = 0
 
                currentLine := false
@@ -496,16 +488,28 @@ func (w *BufWindow) displayBuffer() {
                        s = curNumStyle
                }
 
-               if hasMessage {
-                       w.drawGutter(&vloc, &bloc)
-               }
+               if vloc.Y >= 0 {
+                       if hasMessage {
+                               w.drawGutter(&vloc, &bloc)
+                       }
 
-               if b.Settings["diffgutter"].(bool) {
-                       w.drawDiffGutter(s, false, &vloc, &bloc)
-               }
+                       if b.Settings["diffgutter"].(bool) {
+                               w.drawDiffGutter(s, false, &vloc, &bloc)
+                       }
 
-               if b.Settings["ruler"].(bool) {
-                       w.drawLineNum(s, false, maxLineNumLength, &vloc, &bloc)
+                       if b.Settings["ruler"].(bool) {
+                               w.drawLineNum(s, false, maxLineNumLength, &vloc, &bloc)
+                       }
+               } else {
+                       if hasMessage {
+                               vloc.X += 2
+                       }
+                       if b.Settings["diffgutter"].(bool) {
+                               vloc.X++
+                       }
+                       if b.Settings["ruler"].(bool) {
+                               vloc.X += maxLineNumLength + 1
+                       }
                }
 
                w.gutterOffset = vloc.X
@@ -517,7 +521,7 @@ func (w *BufWindow) displayBuffer() {
                bloc.X = bslice
 
                draw := func(r rune, combc []rune, style tcell.Style, showcursor bool) {
-                       if nColsBeforeStart <= 0 {
+                       if nColsBeforeStart <= 0 && vloc.Y >= 0 {
                                _, origBg, _ := style.Decompose()
                                _, defBg, _ := config.DefStyle.Decompose()
 
@@ -590,6 +594,8 @@ func (w *BufWindow) displayBuffer() {
                                                }
                                        }
                                }
+                       }
+                       if nColsBeforeStart <= 0 {
                                vloc.X++
                        }
                        nColsBeforeStart--
@@ -735,7 +741,7 @@ func (w *BufWindow) displayScrollBar() {
                if barsize < 1 {
                        barsize = 1
                }
-               barstart := w.Y + int(float64(w.StartLine)/float64(w.Buf.LinesNum())*float64(w.Height))
+               barstart := w.Y + int(float64(w.StartLine.Line)/float64(w.Buf.LinesNum())*float64(w.Height))
 
                scrollBarStyle := config.DefStyle.Reverse(true)
                if style, ok := config.Colorscheme["scrollbar"]; ok {
index a5d02c7b67993e46ffa251622993f4a3b19f03d8..4cfbca32b11ab0f9e2cdfb3cb97f2127bd5f72f4 100644 (file)
@@ -72,6 +72,10 @@ func (i *InfoWindow) LocFromVisual(vloc buffer.Loc) buffer.Loc {
        return buffer.Loc{c.GetCharPosInLine(l, vloc.X-n), 0}
 }
 
+func (i *InfoWindow) Scroll(s SLoc, n int) SLoc       { return s }
+func (i *InfoWindow) Diff(s1, s2 SLoc) int            { return 0 }
+func (i *InfoWindow) SLocFromLoc(loc buffer.Loc) SLoc { return SLoc{0, 0} }
+
 func (i *InfoWindow) Clear() {
        for x := 0; x < i.Width; x++ {
                screen.SetContent(x, i.Y, ' ', nil, i.defStyle())
diff --git a/internal/display/softwrap.go b/internal/display/softwrap.go
new file mode 100644 (file)
index 0000000..0f99b52
--- /dev/null
@@ -0,0 +1,149 @@
+package display
+
+import (
+       "github.com/zyedidia/micro/v2/internal/buffer"
+       "github.com/zyedidia/micro/v2/internal/util"
+)
+
+// SLoc represents a vertical scrolling location, i.e. a location of a visual line
+// in the buffer. When softwrap is enabled, a buffer line may be displayed as
+// multiple visual lines (rows). So SLoc stores a number of a line in the buffer
+// and a number of a row within this line.
+type SLoc struct {
+       Line, Row int
+}
+
+// LessThan returns true if s is less b
+func (s SLoc) LessThan(b SLoc) bool {
+       if s.Line < b.Line {
+               return true
+       }
+       return s.Line == b.Line && s.Row < b.Row
+}
+
+// GreaterThan returns true if s is bigger than b
+func (s SLoc) GreaterThan(b SLoc) bool {
+       if s.Line > b.Line {
+               return true
+       }
+       return s.Line == b.Line && s.Row > b.Row
+}
+
+type SoftWrap interface {
+       Scroll(s SLoc, n int) SLoc
+       Diff(s1, s2 SLoc) int
+       SLocFromLoc(loc buffer.Loc) SLoc
+}
+
+func (w *BufWindow) getRow(loc buffer.Loc) int {
+       width := w.Width - w.gutterOffset
+       if w.Buf.Settings["scrollbar"].(bool) && w.Buf.LinesNum() > w.Height {
+               width--
+       }
+       if width <= 0 {
+               return 0
+       }
+       // TODO: this doesn't work quite correctly if there is an incomplete tab
+       // or wide character at the end of a row. See also issue #1979
+       x := util.StringWidth(w.Buf.LineBytes(loc.Y), loc.X, util.IntOpt(w.Buf.Settings["tabsize"]))
+       return x / width
+}
+
+func (w *BufWindow) getRowCount(line int) int {
+       return w.getRow(buffer.Loc{X: util.CharacterCount(w.Buf.LineBytes(line)), Y: line}) + 1
+}
+
+func (w *BufWindow) scrollUp(s SLoc, n int) SLoc {
+       for n > 0 {
+               if n <= s.Row {
+                       s.Row -= n
+                       n = 0
+               } else if s.Line > 0 {
+                       s.Line--
+                       n -= s.Row + 1
+                       s.Row = w.getRowCount(s.Line) - 1
+               } else {
+                       s.Row = 0
+                       break
+               }
+       }
+       return s
+}
+
+func (w *BufWindow) scrollDown(s SLoc, n int) SLoc {
+       for n > 0 {
+               rc := w.getRowCount(s.Line)
+               if n < rc-s.Row {
+                       s.Row += n
+                       n = 0
+               } else if s.Line < w.Buf.LinesNum()-1 {
+                       s.Line++
+                       n -= rc - s.Row
+                       s.Row = 0
+               } else {
+                       s.Row = rc - 1
+                       break
+               }
+       }
+       return s
+}
+
+func (w *BufWindow) scroll(s SLoc, n int) SLoc {
+       if n < 0 {
+               return w.scrollUp(s, -n)
+       }
+       return w.scrollDown(s, n)
+}
+
+func (w *BufWindow) diff(s1, s2 SLoc) int {
+       n := 0
+       for s1.LessThan(s2) {
+               if s1.Line < s2.Line {
+                       n += w.getRowCount(s1.Line) - s1.Row
+                       s1.Line++
+                       s1.Row = 0
+               } else {
+                       n += s2.Row - s1.Row
+                       s1.Row = s2.Row
+               }
+       }
+       return n
+}
+
+// Scroll returns the location which is n visual lines below the location s
+// i.e. the result of scrolling n lines down. n can be negative,
+// which means scrolling up. The returned location is guaranteed to be
+// within the buffer boundaries.
+func (w *BufWindow) Scroll(s SLoc, n int) SLoc {
+       if !w.Buf.Settings["softwrap"].(bool) {
+               s.Line += n
+               if s.Line < 0 {
+                       s.Line = 0
+               }
+               if s.Line > w.Buf.LinesNum()-1 {
+                       s.Line = w.Buf.LinesNum() - 1
+               }
+               return s
+       }
+       return w.scroll(s, n)
+}
+
+// Diff returns the difference (the vertical distance) between two SLocs.
+func (w *BufWindow) Diff(s1, s2 SLoc) int {
+       if !w.Buf.Settings["softwrap"].(bool) {
+               return s2.Line - s1.Line
+       }
+       if s1.GreaterThan(s2) {
+               return -w.diff(s2, s1)
+       }
+       return w.diff(s1, s2)
+}
+
+// SLocFromLoc takes a position in the buffer and returns the location
+// of the visual line containing this position.
+func (w *BufWindow) SLocFromLoc(loc buffer.Loc) SLoc {
+       if !w.Buf.Settings["softwrap"].(bool) {
+               return SLoc{loc.Y, 0}
+       }
+       return SLoc{loc.Y, w.getRow(loc)}
+}
index 56787fff4ef3d2bdf898fe7541edb353dd4faabf..eb71970f6eabb34607565601aac88c26458cdce4 100644 (file)
@@ -8,10 +8,13 @@ type View struct {
        X, Y          int // X,Y location of the view
        Width, Height int // Width and height of the view
 
-       // Start line and start column of the view (vertical/horizontal scroll)
+       // Start line of the view (for vertical scroll)
+       StartLine SLoc
+
+       // Start column of the view (for horizontal scroll)
        // note that since the starting column of every line is different if the view
        // is scrolled, StartCol is a visual index (will be the same for every line)
-       StartLine, StartCol int
+       StartCol int
 }
 
 type Window interface {
@@ -28,5 +31,6 @@ type Window interface {
 
 type BWindow interface {
        Window
+       SoftWrap
        SetBuffer(b *buffer.Buffer)
 }
index 1818b96bddd4d11f677f4b4439c6556f2d214830..f671f776371be24156cbb8714d9e67c397fb5f2c 100644 (file)
@@ -259,6 +259,7 @@ The packages and functions are listed below (in Go type signatures):
     - `MTError` error message.
 
     - `Loc(x, y int) Loc`: creates a new location struct.
+    - `SLoc(line, row int) display.SLoc`: creates a new scrolling location struct.
 
     - `BTDefault`: default buffer type.
     - `BTLog`: log buffer type.