From c5798b5b8cef385f9f34f256239c855e3db94253 Mon Sep 17 00:00:00 2001 From: Dmitry Maluka Date: Wed, 7 Apr 2021 22:18:51 +0200 Subject: [PATCH] Fix softwrap scrolling issues (#1981) 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 | 3 + internal/action/actions.go | 75 ++++++----------- internal/action/globals.go | 15 ---- internal/display/bufwindow.go | 96 +++++++++++---------- internal/display/infowindow.go | 4 + internal/display/softwrap.go | 149 +++++++++++++++++++++++++++++++++ internal/display/window.go | 8 +- runtime/help/plugins.md | 1 + 8 files changed, 240 insertions(+), 111 deletions(-) create mode 100644 internal/display/softwrap.go diff --git a/cmd/micro/initlua.go b/cmd/micro/initlua.go index 2ed94c3d..dcb60830 100644 --- a/cmd/micro/initlua.go +++ b/cmd/micro/initlua.go @@ -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)) diff --git a/internal/action/actions.go b/internal/action/actions.go index 44eadbf8..6db8e822 100644 --- a/internal/action/actions.go +++ b/internal/action/actions.go @@ -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 } diff --git a/internal/action/globals.go b/internal/action/globals.go index 4a3b8375..e20f61ed 100644 --- a/internal/action/globals.go +++ b/internal/action/globals.go @@ -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) - } } diff --git a/internal/display/bufwindow.go b/internal/display/bufwindow.go index afcfa5bc..2fcf0ac5 100644 --- a/internal/display/bufwindow.go +++ b/internal/display/bufwindow.go @@ -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 { diff --git a/internal/display/infowindow.go b/internal/display/infowindow.go index a5d02c7b..4cfbca32 100644 --- a/internal/display/infowindow.go +++ b/internal/display/infowindow.go @@ -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 index 00000000..0f99b526 --- /dev/null +++ b/internal/display/softwrap.go @@ -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)} +} diff --git a/internal/display/window.go b/internal/display/window.go index 56787fff..eb71970f 100644 --- a/internal/display/window.go +++ b/internal/display/window.go @@ -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) } diff --git a/runtime/help/plugins.md b/runtime/help/plugins.md index 1818b96b..f671f776 100644 --- a/runtime/help/plugins.md +++ b/runtime/help/plugins.md @@ -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.