]> git.lizzy.rs Git - micro.git/blobdiff - internal/display/softwrap.go
Merge pull request #2076 from dmaluka/softwrap-improvement2
[micro.git] / internal / display / softwrap.go
index 0f99b526bfa3068f21d19f3fb098d4201ba52644..0597f061604e97e37ac0377ef8f7e2179228c72b 100644 (file)
@@ -1,6 +1,7 @@
 package display
 
 import (
+       runewidth "github.com/mattn/go-runewidth"
        "github.com/zyedidia/micro/v2/internal/buffer"
        "github.com/zyedidia/micro/v2/internal/util"
 )
@@ -29,28 +30,180 @@ func (s SLoc) GreaterThan(b SLoc) bool {
        return s.Line == b.Line && s.Row > b.Row
 }
 
+// VLoc represents a location in the buffer as a visual location in the
+// linewrapped buffer.
+type VLoc struct {
+       SLoc
+       VisualX int
+}
+
 type SoftWrap interface {
        Scroll(s SLoc, n int) SLoc
        Diff(s1, s2 SLoc) int
        SLocFromLoc(loc buffer.Loc) SLoc
+       VLocFromLoc(loc buffer.Loc) VLoc
+       LocFromVLoc(vloc VLoc) buffer.Loc
 }
 
-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--
+func (w *BufWindow) getVLocFromLoc(loc buffer.Loc) VLoc {
+       vloc := VLoc{SLoc: SLoc{loc.Y, 0}, VisualX: 0}
+
+       if loc.X <= 0 {
+               return vloc
        }
-       if width <= 0 {
-               return 0
+
+       if w.bufWidth <= 0 {
+               return vloc
+       }
+
+       wordwrap := w.Buf.Settings["wordwrap"].(bool)
+       tabsize := util.IntOpt(w.Buf.Settings["tabsize"])
+
+       line := w.Buf.LineBytes(loc.Y)
+       x := 0
+       totalwidth := 0
+
+       wordwidth := 0
+       wordoffset := 0
+
+       for len(line) > 0 {
+               r, _, size := util.DecodeCharacter(line)
+               line = line[size:]
+
+               width := 0
+               switch r {
+               case '\t':
+                       ts := tabsize - (totalwidth % tabsize)
+                       width = util.Min(ts, w.bufWidth-vloc.VisualX)
+                       totalwidth += ts
+               default:
+                       width = runewidth.RuneWidth(r)
+                       totalwidth += width
+               }
+
+               wordwidth += width
+
+               // Collect a complete word to know its width.
+               // If wordwrap is off, every single character is a complete "word".
+               if wordwrap {
+                       if !util.IsWhitespace(r) && len(line) > 0 && wordwidth < w.bufWidth {
+                               if x < loc.X {
+                                       wordoffset += width
+                                       x++
+                               }
+                               continue
+                       }
+               }
+
+               // If a word (or just a wide rune) does not fit in the window
+               if vloc.VisualX+wordwidth > w.bufWidth && vloc.VisualX > 0 {
+                       vloc.Row++
+                       vloc.VisualX = 0
+               }
+
+               if x == loc.X {
+                       vloc.VisualX += wordoffset
+                       return vloc
+               }
+               x++
+
+               vloc.VisualX += wordwidth
+
+               wordwidth = 0
+               wordoffset = 0
+
+               if vloc.VisualX >= w.bufWidth {
+                       vloc.Row++
+                       vloc.VisualX = 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
+       return vloc
+}
+
+func (w *BufWindow) getLocFromVLoc(svloc VLoc) buffer.Loc {
+       loc := buffer.Loc{X: 0, Y: svloc.Line}
+
+       if w.bufWidth <= 0 {
+               return loc
+       }
+
+       wordwrap := w.Buf.Settings["wordwrap"].(bool)
+       tabsize := util.IntOpt(w.Buf.Settings["tabsize"])
+
+       line := w.Buf.LineBytes(svloc.Line)
+       vloc := VLoc{SLoc: SLoc{svloc.Line, 0}, VisualX: 0}
+
+       totalwidth := 0
+
+       var widths []int
+       if wordwrap {
+               widths = make([]int, 0, w.bufWidth)
+       } else {
+               widths = make([]int, 0, 1)
+       }
+       wordwidth := 0
+
+       for len(line) > 0 {
+               r, _, size := util.DecodeCharacter(line)
+               line = line[size:]
+
+               width := 0
+               switch r {
+               case '\t':
+                       ts := tabsize - (totalwidth % tabsize)
+                       width = util.Min(ts, w.bufWidth-vloc.VisualX)
+                       totalwidth += ts
+               default:
+                       width = runewidth.RuneWidth(r)
+                       totalwidth += width
+               }
+
+               widths = append(widths, width)
+               wordwidth += width
+
+               // Collect a complete word to know its width.
+               // If wordwrap is off, every single character is a complete "word".
+               if wordwrap {
+                       if !util.IsWhitespace(r) && len(line) > 0 && wordwidth < w.bufWidth {
+                               continue
+                       }
+               }
+
+               // If a word (or just a wide rune) does not fit in the window
+               if vloc.VisualX+wordwidth > w.bufWidth && vloc.VisualX > 0 {
+                       if vloc.Row == svloc.Row {
+                               if wordwrap {
+                                       // it's a word, not a wide rune
+                                       loc.X--
+                               }
+                               return loc
+                       }
+                       vloc.Row++
+                       vloc.VisualX = 0
+               }
+
+               for i := range widths {
+                       vloc.VisualX += widths[i]
+                       if vloc.Row == svloc.Row && vloc.VisualX > svloc.VisualX {
+                               return loc
+                       }
+                       loc.X++
+               }
+
+               widths = widths[:0]
+               wordwidth = 0
+
+               if vloc.VisualX >= w.bufWidth {
+                       vloc.Row++
+                       vloc.VisualX = 0
+               }
+       }
+       return loc
 }
 
 func (w *BufWindow) getRowCount(line int) int {
-       return w.getRow(buffer.Loc{X: util.CharacterCount(w.Buf.LineBytes(line)), Y: line}) + 1
+       eol := buffer.Loc{X: util.CharacterCount(w.Buf.LineBytes(line)), Y: line}
+       return w.getVLocFromLoc(eol).Row + 1
 }
 
 func (w *BufWindow) scrollUp(s SLoc, n int) SLoc {
@@ -145,5 +298,29 @@ 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)}
+       return w.getVLocFromLoc(loc).SLoc
+}
+
+// VLocFromLoc takes a position in the buffer and returns the corresponding
+// visual location in the linewrapped buffer.
+func (w *BufWindow) VLocFromLoc(loc buffer.Loc) VLoc {
+       if !w.Buf.Settings["softwrap"].(bool) {
+               tabsize := util.IntOpt(w.Buf.Settings["tabsize"])
+
+               visualx := util.StringWidth(w.Buf.LineBytes(loc.Y), loc.X, tabsize)
+               return VLoc{SLoc{loc.Y, 0}, visualx}
+       }
+       return w.getVLocFromLoc(loc)
+}
+
+// LocFromVLoc takes a visual location in the linewrapped buffer and returns
+// the position in the buffer corresponding to this visual location.
+func (w *BufWindow) LocFromVLoc(vloc VLoc) buffer.Loc {
+       if !w.Buf.Settings["softwrap"].(bool) {
+               tabsize := util.IntOpt(w.Buf.Settings["tabsize"])
+
+               x := util.GetCharPosInLine(w.Buf.LineBytes(vloc.Line), vloc.VisualX, tabsize)
+               return buffer.Loc{x, vloc.Line}
+       }
+       return w.getLocFromVLoc(vloc)
 }