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.