]> 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 fa0f92c844e8b37c80ed48f7f89001b949ed7b47..0597f061604e97e37ac0377ef8f7e2179228c72b 100644 (file)
@@ -30,66 +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 {
+func (w *BufWindow) getVLocFromLoc(loc buffer.Loc) VLoc {
+       vloc := VLoc{SLoc: SLoc{loc.Y, 0}, VisualX: 0}
+
        if loc.X <= 0 {
-               return 0
+               return vloc
        }
 
        if w.bufWidth <= 0 {
-               return 0
+               return vloc
        }
 
+       wordwrap := w.Buf.Settings["wordwrap"].(bool)
        tabsize := util.IntOpt(w.Buf.Settings["tabsize"])
 
        line := w.Buf.LineBytes(loc.Y)
        x := 0
-       visualx := 0
-       row := 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-visualx)
+                       width = util.Min(ts, w.bufWidth-vloc.VisualX)
                        totalwidth += ts
                default:
                        width = runewidth.RuneWidth(r)
                        totalwidth += width
                }
 
-               // If a wide rune does not fit in the window
-               if visualx+width > w.bufWidth && visualx > 0 {
-                       row++
-                       visualx = 0
+               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 {
-                       return row
+                       vloc.VisualX += wordoffset
+                       return vloc
                }
                x++
+
+               vloc.VisualX += wordwidth
+
+               wordwidth = 0
+               wordoffset = 0
+
+               if vloc.VisualX >= w.bufWidth {
+                       vloc.Row++
+                       vloc.VisualX = 0
+               }
+       }
+       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:]
 
-               visualx += width
-               if visualx >= w.bufWidth {
-                       row++
-                       visualx = 0
+               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 row
+       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 {
@@ -184,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)
 }