From 965e43ebf1319947a325f252000985fb0a586449 Mon Sep 17 00:00:00 2001 From: Dmitry Maluka Date: Wed, 17 Mar 2021 22:34:30 +0100 Subject: [PATCH] Implement word wrapping Fixes #264 Fixes #1644 --- internal/config/settings.go | 1 + internal/display/bufwindow.go | 63 +++++++++++++++++++++++------ internal/display/softwrap.go | 74 +++++++++++++++++++++++++++++------ runtime/help/options.md | 5 +++ 4 files changed, 119 insertions(+), 24 deletions(-) diff --git a/internal/config/settings.go b/internal/config/settings.go index 7ccd3516..c294b13f 100644 --- a/internal/config/settings.go +++ b/internal/config/settings.go @@ -297,6 +297,7 @@ var defaultCommonSettings = map[string]interface{}{ "tabsize": float64(4), "tabstospaces": false, "useprimary": true, + "wordwrap": false, } func GetInfoBarOffset() int { diff --git a/internal/display/bufwindow.go b/internal/display/bufwindow.go index 5f95054f..4ccd8d8d 100644 --- a/internal/display/bufwindow.go +++ b/internal/display/bufwindow.go @@ -420,6 +420,8 @@ func (w *BufWindow) displayBuffer() { } softwrap := b.Settings["softwrap"].(bool) + wordwrap := softwrap && b.Settings["wordwrap"].(bool) + tabsize := util.IntOpt(b.Settings["tabsize"]) colorcolumn := util.IntOpt(b.Settings["colorcolumn"]) @@ -571,15 +573,31 @@ func (w *BufWindow) displayBuffer() { } } + type glyph struct { + r rune + combc []rune + style tcell.Style + width int + } + + var word []glyph + if wordwrap { + word = make([]glyph, 0, w.bufWidth) + } else { + word = make([]glyph, 0, 1) + } + wordwidth := 0 + totalwidth := w.StartCol - nColsBeforeStart for len(line) > 0 { r, combc, size := util.DecodeCharacter(line) + line = line[size:] - curStyle, _ = w.getStyle(curStyle, bloc) + loc := buffer.Loc{X: bloc.X + len(word), Y: bloc.Y} + curStyle, _ = w.getStyle(curStyle, loc) width := 0 - char := ' ' switch r { case '\t': ts := tabsize - (totalwidth % tabsize) @@ -587,17 +605,27 @@ func (w *BufWindow) displayBuffer() { totalwidth += ts default: width = runewidth.RuneWidth(r) - char = '@' totalwidth += width } - // If a wide rune does not fit in the window - if vloc.X+width > maxWidth && vloc.X > w.gutterOffset { + word = append(word, glyph{r, combc, curStyle, 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.X+wordwidth > maxWidth && vloc.X > w.gutterOffset { for vloc.X < maxWidth { draw(' ', nil, config.DefStyle, false) } - // We either stop or we wrap to draw the rune in the next line + // We either stop or we wrap to draw the word in the next line if !softwrap { break } else { @@ -609,16 +637,25 @@ func (w *BufWindow) displayBuffer() { } } - draw(r, combc, curStyle, true) + for _, r := range word { + draw(r.r, r.combc, r.style, true) - // Draw any extra characters either spaces for tabs or @ for incomplete wide runes - if width > 1 { - for i := 1; i < width; i++ { - draw(char, nil, curStyle, false) + // Draw any extra characters either spaces for tabs or @ for incomplete wide runes + if r.width > 1 { + char := ' ' + if r.r != '\t' { + char = '@' + } + + for i := 1; i < r.width; i++ { + draw(char, nil, r.style, false) + } } + bloc.X++ } - bloc.X++ - line = line[size:] + + word = word[:0] + wordwidth = 0 // If we reach the end of the window then we either stop or we wrap for softwrap if vloc.X >= maxWidth { diff --git a/internal/display/softwrap.go b/internal/display/softwrap.go index 2cbfe175..0597f061 100644 --- a/internal/display/softwrap.go +++ b/internal/display/softwrap.go @@ -56,14 +56,19 @@ func (w *BufWindow) getVLocFromLoc(loc buffer.Loc) VLoc { 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 { @@ -76,19 +81,37 @@ func (w *BufWindow) getVLocFromLoc(loc buffer.Loc) VLoc { totalwidth += width } - // If a wide rune does not fit in the window - if vloc.VisualX+width > w.bufWidth && vloc.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 { + vloc.VisualX += wordoffset return vloc } x++ - line = line[size:] - vloc.VisualX += width + vloc.VisualX += wordwidth + + wordwidth = 0 + wordoffset = 0 + if vloc.VisualX >= w.bufWidth { vloc.Row++ vloc.VisualX = 0 @@ -104,6 +127,7 @@ func (w *BufWindow) getLocFromVLoc(svloc VLoc) buffer.Loc { return loc } + wordwrap := w.Buf.Settings["wordwrap"].(bool) tabsize := util.IntOpt(w.Buf.Settings["tabsize"]) line := w.Buf.LineBytes(svloc.Line) @@ -111,8 +135,17 @@ func (w *BufWindow) getLocFromVLoc(svloc VLoc) buffer.Loc { 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 { @@ -125,21 +158,40 @@ func (w *BufWindow) getLocFromVLoc(svloc VLoc) buffer.Loc { totalwidth += width } - // If a wide rune does not fit in the window - if vloc.VisualX+width > w.bufWidth && vloc.VisualX > 0 { + 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 } - vloc.VisualX += width - if vloc.Row == svloc.Row && vloc.VisualX > svloc.VisualX { - return loc + for i := range widths { + vloc.VisualX += widths[i] + if vloc.Row == svloc.Row && vloc.VisualX > svloc.VisualX { + return loc + } + loc.X++ } - loc.X++ - line = line[size:] + + widths = widths[:0] + wordwidth = 0 if vloc.VisualX >= w.bufWidth { vloc.Row++ diff --git a/runtime/help/options.md b/runtime/help/options.md index 3805de7f..491eaba5 100644 --- a/runtime/help/options.md +++ b/runtime/help/options.md @@ -365,6 +365,11 @@ Here are the available options: default value: `true` +* `wordwrap`: wrap long lines by words, i.e. break at spaces. This option + only does anything if `softwrap` is on. + + default value: `false` + * `xterm`: micro will assume that the terminal it is running in conforms to `xterm-256color` regardless of what the `$TERM` variable actually contains. Enabling this option may cause unwanted effects if your terminal in fact