From c960c93a83214f34e68de83db11eff65a15fb783 Mon Sep 17 00:00:00 2001 From: Dmitry Maluka Date: Sat, 27 Feb 2021 17:37:15 +0100 Subject: [PATCH 01/11] Add BufWidth and BufHeight Fixes issue with the usage of a slightly incorrect buffer height value (v.Height should be v.Height-1 if statusline is displayed). Also, to avoid too many duplications, the code reorganized a little: buffer display params (width, height, gutter offset and others) are calculated in a single place. --- internal/action/actions.go | 28 +++--- internal/display/bufwindow.go | 172 ++++++++++++++++----------------- internal/display/infowindow.go | 3 + internal/display/softwrap.go | 8 +- internal/display/window.go | 2 + 5 files changed, 104 insertions(+), 109 deletions(-) diff --git a/internal/action/actions.go b/internal/action/actions.go index 6db8e822..c0334a6a 100644 --- a/internal/action/actions.go +++ b/internal/action/actions.go @@ -36,8 +36,8 @@ func (h *BufPane) ScrollDown(n int) { 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) + if h.Diff(v.StartLine, end) < h.BufHeight()-1 { + v.StartLine = h.Scroll(end, -h.BufHeight()+1) } h.SetView(v) } @@ -117,7 +117,7 @@ func (h *BufPane) ScrollDownAction() bool { // Center centers the view on the cursor func (h *BufPane) Center() bool { v := h.GetView() - v.StartLine = h.Scroll(h.SLocFromLoc(h.Cursor.Loc), -v.Height/2) + v.StartLine = h.Scroll(h.SLocFromLoc(h.Cursor.Loc), -h.BufHeight()/2) h.SetView(v) h.ScrollAdjust() return true @@ -1251,22 +1251,20 @@ func (h *BufPane) Start() bool { // End moves the viewport to the end of the buffer func (h *BufPane) End() bool { v := h.GetView() - v.StartLine = h.Scroll(h.SLocFromLoc(h.Buf.End()), -v.Height+1) + v.StartLine = h.Scroll(h.SLocFromLoc(h.Buf.End()), -h.BufHeight()+1) h.SetView(v) return true } // PageUp scrolls the view up a page func (h *BufPane) PageUp() bool { - v := h.GetView() - h.ScrollUp(v.Height) + h.ScrollUp(h.BufHeight()) return true } // PageDown scrolls the view down a page func (h *BufPane) PageDown() bool { - v := h.GetView() - h.ScrollDown(v.Height) + h.ScrollDown(h.BufHeight()) h.ScrollAdjust() return true } @@ -1276,7 +1274,7 @@ func (h *BufPane) SelectPageUp() bool { if !h.Cursor.HasSelection() { h.Cursor.OrigSelection[0] = h.Cursor.Loc } - h.Cursor.UpN(h.GetView().Height) + h.Cursor.UpN(h.BufHeight()) h.Cursor.SelectTo(h.Cursor.Loc) h.Relocate() return true @@ -1287,7 +1285,7 @@ func (h *BufPane) SelectPageDown() bool { if !h.Cursor.HasSelection() { h.Cursor.OrigSelection[0] = h.Cursor.Loc } - h.Cursor.DownN(h.GetView().Height) + h.Cursor.DownN(h.BufHeight()) h.Cursor.SelectTo(h.Cursor.Loc) h.Relocate() return true @@ -1302,7 +1300,7 @@ func (h *BufPane) CursorPageUp() bool { h.Cursor.ResetSelection() h.Cursor.StoreVisualX() } - h.Cursor.UpN(h.GetView().Height) + h.Cursor.UpN(h.BufHeight()) h.Relocate() return true } @@ -1316,22 +1314,20 @@ func (h *BufPane) CursorPageDown() bool { h.Cursor.ResetSelection() h.Cursor.StoreVisualX() } - h.Cursor.DownN(h.GetView().Height) + h.Cursor.DownN(h.BufHeight()) h.Relocate() return true } // HalfPageUp scrolls the view up half a page func (h *BufPane) HalfPageUp() bool { - v := h.GetView() - h.ScrollUp(v.Height / 2) + h.ScrollUp(h.BufHeight() / 2) return true } // HalfPageDown scrolls the view down half a page func (h *BufPane) HalfPageDown() bool { - v := h.GetView() - h.ScrollDown(v.Height / 2) + h.ScrollDown(h.BufHeight() / 2) h.ScrollAdjust() return true } diff --git a/internal/display/bufwindow.go b/internal/display/bufwindow.go index 2fcf0ac5..3c1136b1 100644 --- a/internal/display/bufwindow.go +++ b/internal/display/bufwindow.go @@ -23,8 +23,12 @@ type BufWindow struct { sline *StatusLine - gutterOffset int - drawStatus bool + bufWidth int + bufHeight int + gutterOffset int + hasMessage bool + maxLineNumLength int + drawDivider bool } // NewBufWindow creates a new window at a location in the screen with a width and height @@ -64,6 +68,61 @@ func (w *BufWindow) IsActive() bool { return w.active } +// BufWidth returns the width of the actual buffer displayed in the window, +// which is usually less than the window width due to the gutter, ruler or scrollbar +func (w *BufWindow) BufWidth() int { + return w.bufWidth +} + +// BufHeight returns the height of the actual buffer displayed in the window, +// which is usually less than the window height due to the statusline +func (w *BufWindow) BufHeight() int { + return w.bufHeight +} + +func (w *BufWindow) updateDisplayInfo() { + b := w.Buf + + w.drawDivider = false + if !b.Settings["statusline"].(bool) { + _, h := screen.Screen.Size() + infoY := h + if config.GetGlobalOption("infobar").(bool) { + infoY-- + } + if w.Y+w.Height != infoY { + w.drawDivider = true + } + } + + w.bufHeight = w.Height + if b.Settings["statusline"].(bool) || w.drawDivider { + w.bufHeight-- + } + + w.hasMessage = len(b.Messages) > 0 + + // We need to know the string length of the largest line number + // so we can pad appropriately when displaying line numbers + w.maxLineNumLength = len(strconv.Itoa(b.LinesNum())) + + w.gutterOffset = 0 + if w.hasMessage { + w.gutterOffset += 2 + } + if b.Settings["diffgutter"].(bool) { + w.gutterOffset++ + } + if b.Settings["ruler"].(bool) { + w.gutterOffset += w.maxLineNumLength + 1 + } + + w.bufWidth = w.Width - w.gutterOffset + if w.Buf.Settings["scrollbar"].(bool) && w.Buf.LinesNum() > w.Height { + w.bufWidth-- + } +} + func (w *BufWindow) getStartInfo(n, lineN int) ([]byte, int, int, *tcell.Style) { tabsize := util.IntOpt(w.Buf.Settings["tabsize"]) width := 0 @@ -111,10 +170,7 @@ func (w *BufWindow) Clear() { // Returns true if the window location is moved func (w *BufWindow) Relocate() bool { b := w.Buf - height := w.Height - if w.drawStatus { - height-- - } + height := w.bufHeight ret := false activeC := w.Buf.GetActiveCursor() scrollmargin := int(b.Settings["scrollmargin"].(float64)) @@ -162,20 +218,7 @@ func (w *BufWindow) Relocate() bool { func (w *BufWindow) LocFromVisual(svloc buffer.Loc) buffer.Loc { b := w.Buf - hasMessage := len(b.Messages) > 0 - bufHeight := w.Height - if w.drawStatus { - bufHeight-- - } - - bufWidth := w.Width - if w.Buf.Settings["scrollbar"].(bool) && w.Buf.LinesNum() > w.Height { - bufWidth-- - } - - // We need to know the string length of the largest line number - // so we can pad appropriately when displaying line numbers - maxLineNumLength := len(strconv.Itoa(b.LinesNum())) + maxWidth := w.gutterOffset + w.bufWidth tabsize := int(b.Settings["tabsize"].(float64)) softwrap := b.Settings["softwrap"].(bool) @@ -191,17 +234,8 @@ func (w *BufWindow) LocFromVisual(svloc buffer.Loc) buffer.Loc { // this represents the current draw position in the buffer (char positions) bloc := buffer.Loc{X: -1, Y: w.StartLine.Line} - for ; vloc.Y < bufHeight; vloc.Y++ { - vloc.X = 0 - if hasMessage { - vloc.X += 2 - } - if b.Settings["diffgutter"].(bool) { - vloc.X++ - } - if b.Settings["ruler"].(bool) { - vloc.X += maxLineNumLength + 1 - } + for ; vloc.Y < w.bufHeight; vloc.Y++ { + vloc.X = w.gutterOffset line := b.LineBytes(bloc.Y) line, nColsBeforeStart, bslice := util.SliceVisualEnd(line, w.StartCol, tabsize) @@ -251,12 +285,12 @@ func (w *BufWindow) LocFromVisual(svloc buffer.Loc) buffer.Loc { totalwidth += width // If we reach the end of the window then we either stop or we wrap for softwrap - if vloc.X >= bufWidth { + if vloc.X >= maxWidth { if !softwrap { break } else { vloc.Y++ - if vloc.Y >= bufHeight { + if vloc.Y >= w.bufHeight { break } vloc.X = w.gutterOffset @@ -267,7 +301,7 @@ func (w *BufWindow) LocFromVisual(svloc buffer.Loc) buffer.Loc { return bloc } - if bloc.Y+1 >= b.LinesNum() || vloc.Y+1 >= bufHeight { + if bloc.Y+1 >= b.LinesNum() || vloc.Y+1 >= w.bufHeight { return bloc } @@ -322,7 +356,7 @@ func (w *BufWindow) drawDiffGutter(backgroundStyle tcell.Style, softwrapped bool vloc.X++ } -func (w *BufWindow) drawLineNum(lineNumStyle tcell.Style, softwrapped bool, maxLineNumLength int, vloc *buffer.Loc, bloc *buffer.Loc) { +func (w *BufWindow) drawLineNum(lineNumStyle tcell.Style, softwrapped bool, vloc *buffer.Loc, bloc *buffer.Loc) { cursorLine := w.Buf.GetActiveCursor().Loc.Y var lineInt int if w.Buf.Settings["relativeruler"] == false || cursorLine == bloc.Y { @@ -333,7 +367,7 @@ func (w *BufWindow) drawLineNum(lineNumStyle tcell.Style, softwrapped bool, maxL lineNum := strconv.Itoa(util.Abs(lineInt)) // Write the spaces before the line number if necessary - for i := 0; i < maxLineNumLength-len(lineNum); i++ { + for i := 0; i < w.maxLineNumLength-len(lineNum); i++ { screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, ' ', nil, lineNumStyle) vloc.X++ } @@ -380,16 +414,7 @@ func (w *BufWindow) displayBuffer() { return } - hasMessage := len(b.Messages) > 0 - bufHeight := w.Height - if w.drawStatus { - bufHeight-- - } - - bufWidth := w.Width - if w.Buf.Settings["scrollbar"].(bool) && w.Buf.LinesNum() > w.Height { - bufWidth-- - } + maxWidth := w.gutterOffset + w.bufWidth if b.ModifiedThisFrame { if b.Settings["diffgutter"].(bool) { @@ -450,10 +475,6 @@ func (w *BufWindow) displayBuffer() { } } - // We need to know the string length of the largest line number - // so we can pad appropriately when displaying line numbers - maxLineNumLength := len(strconv.Itoa(b.LinesNum())) - softwrap := b.Settings["softwrap"].(bool) tabsize := util.IntOpt(b.Settings["tabsize"]) colorcolumn := util.IntOpt(b.Settings["colorcolumn"]) @@ -472,7 +493,7 @@ func (w *BufWindow) displayBuffer() { cursors := b.GetCursors() curStyle := config.DefStyle - for ; vloc.Y < bufHeight; vloc.Y++ { + for ; vloc.Y < w.bufHeight; vloc.Y++ { vloc.X = 0 currentLine := false @@ -489,7 +510,7 @@ func (w *BufWindow) displayBuffer() { } if vloc.Y >= 0 { - if hasMessage { + if w.hasMessage { w.drawGutter(&vloc, &bloc) } @@ -498,22 +519,12 @@ func (w *BufWindow) displayBuffer() { } if b.Settings["ruler"].(bool) { - w.drawLineNum(s, false, maxLineNumLength, &vloc, &bloc) + w.drawLineNum(s, false, &vloc, &bloc) } } else { - if hasMessage { - vloc.X += 2 - } - if b.Settings["diffgutter"].(bool) { - vloc.X++ - } - if b.Settings["ruler"].(bool) { - vloc.X += maxLineNumLength + 1 - } + vloc.X = w.gutterOffset } - w.gutterOffset = vloc.X - line, nColsBeforeStart, bslice, startStyle := w.getStartInfo(w.StartCol, bloc.Y) if startStyle != nil { curStyle = *startStyle @@ -633,16 +644,16 @@ func (w *BufWindow) displayBuffer() { totalwidth += width // If we reach the end of the window then we either stop or we wrap for softwrap - if vloc.X >= bufWidth { + if vloc.X >= maxWidth { if !softwrap { break } else { vloc.Y++ - if vloc.Y >= bufHeight { + if vloc.Y >= w.bufHeight { break } vloc.X = 0 - if hasMessage { + if w.hasMessage { w.drawGutter(&vloc, &bloc) } if b.Settings["diffgutter"].(bool) { @@ -651,7 +662,7 @@ func (w *BufWindow) displayBuffer() { // This will draw an empty line number because the current line is wrapped if b.Settings["ruler"].(bool) { - w.drawLineNum(lineNumStyle, true, maxLineNumLength, &vloc, &bloc) + w.drawLineNum(lineNumStyle, true, &vloc, &bloc) } } } @@ -667,7 +678,7 @@ func (w *BufWindow) displayBuffer() { } } } - for i := vloc.X; i < bufWidth; i++ { + for i := vloc.X; i < maxWidth; i++ { curStyle := style if s, ok := config.Colorscheme["color-column"]; ok { if colorcolumn != 0 && i-w.gutterOffset+w.StartCol == colorcolumn { @@ -678,7 +689,7 @@ func (w *BufWindow) displayBuffer() { screen.SetContent(i+w.X, vloc.Y+w.Y, ' ', nil, curStyle) } - if vloc.X != bufWidth { + if vloc.X != maxWidth { // Display newline within a selection draw(' ', nil, config.DefStyle, true) } @@ -692,18 +703,9 @@ func (w *BufWindow) displayBuffer() { } func (w *BufWindow) displayStatusLine() { - _, h := screen.Screen.Size() - infoY := h - if config.GetGlobalOption("infobar").(bool) { - infoY-- - } - if w.Buf.Settings["statusline"].(bool) { - w.drawStatus = true w.sline.Display() - } else if w.Y+w.Height != infoY { - w.drawStatus = true - + } else if w.drawDivider { divchars := config.GetGlobalOption("divchars").(string) if util.CharacterCountInString(divchars) != 2 { divchars = "|-" @@ -725,18 +727,12 @@ func (w *BufWindow) displayStatusLine() { for x := w.X; x < w.X+w.Width; x++ { screen.SetContent(x, w.Y+w.Height-1, divchar, combc, dividerStyle) } - } else { - w.drawStatus = false } } func (w *BufWindow) displayScrollBar() { if w.Buf.Settings["scrollbar"].(bool) && w.Buf.LinesNum() > w.Height { scrollX := w.X + w.Width - 1 - bufHeight := w.Height - if w.drawStatus { - bufHeight-- - } barsize := int(float64(w.Height) / float64(w.Buf.LinesNum()) * float64(w.Height)) if barsize < 1 { barsize = 1 @@ -748,7 +744,7 @@ func (w *BufWindow) displayScrollBar() { scrollBarStyle = style } - for y := barstart; y < util.Min(barstart+barsize, w.Y+bufHeight); y++ { + for y := barstart; y < util.Min(barstart+barsize, w.Y+w.bufHeight); y++ { screen.SetContent(scrollX, y, '|', nil, scrollBarStyle) } } @@ -756,6 +752,8 @@ func (w *BufWindow) displayScrollBar() { // Display displays the buffer and the statusline func (w *BufWindow) Display() { + w.updateDisplayInfo() + w.displayStatusLine() w.displayScrollBar() w.displayBuffer() diff --git a/internal/display/infowindow.go b/internal/display/infowindow.go index 4cfbca32..530dce67 100644 --- a/internal/display/infowindow.go +++ b/internal/display/infowindow.go @@ -72,6 +72,9 @@ func (i *InfoWindow) LocFromVisual(vloc buffer.Loc) buffer.Loc { return buffer.Loc{c.GetCharPosInLine(l, vloc.X-n), 0} } +func (i *InfoWindow) BufWidth() int { return i.Width } +func (i *InfoWindow) BufHeight() int { return 1 } + 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} } diff --git a/internal/display/softwrap.go b/internal/display/softwrap.go index 0f99b526..bbcc99e0 100644 --- a/internal/display/softwrap.go +++ b/internal/display/softwrap.go @@ -36,17 +36,13 @@ type SoftWrap interface { } 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 { + if w.bufWidth <= 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 + return x / w.bufWidth } func (w *BufWindow) getRowCount(line int) int { diff --git a/internal/display/window.go b/internal/display/window.go index eb71970f..eb2c09f4 100644 --- a/internal/display/window.go +++ b/internal/display/window.go @@ -33,4 +33,6 @@ type BWindow interface { Window SoftWrap SetBuffer(b *buffer.Buffer) + BufWidth() int + BufHeight() int } From a1651aec2f7955078741897d3d9441a51318ca7e Mon Sep 17 00:00:00 2001 From: Dmitry Maluka Date: Wed, 3 Mar 2021 20:51:06 +0100 Subject: [PATCH 02/11] Fix horizontal scrolling issue after toggling softwrap on/off Fixes #645 --- internal/buffer/buffer.go | 7 +++++++ internal/buffer/settings.go | 4 ++++ internal/display/bufwindow.go | 13 ++++++++++++- 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/internal/buffer/buffer.go b/internal/buffer/buffer.go index 84ab7dc0..718510dd 100644 --- a/internal/buffer/buffer.go +++ b/internal/buffer/buffer.go @@ -189,6 +189,13 @@ type Buffer struct { cursors []*Cursor curCursor int StartCursor Loc + + // OptionCallback is called after a buffer option value is changed. + // The display module registers its OptionCallback to ensure the buffer window + // is properly updated when needed. This is a workaround for the fact that + // the buffer module cannot directly call the display's API (it would mean + // a circular dependency between packages). + OptionCallback func(option string, nativeValue interface{}) } // NewBufferFromFileAtLoc opens a new buffer with a given cursor location diff --git a/internal/buffer/settings.go b/internal/buffer/settings.go index e04d7061..c3ff428c 100644 --- a/internal/buffer/settings.go +++ b/internal/buffer/settings.go @@ -41,6 +41,10 @@ func (b *Buffer) SetOptionNative(option string, nativeValue interface{}) error { b.Type.Readonly = nativeValue.(bool) } + if b.OptionCallback != nil { + b.OptionCallback(option, nativeValue) + } + return nil } diff --git a/internal/display/bufwindow.go b/internal/display/bufwindow.go index 3c1136b1..0a9e174d 100644 --- a/internal/display/bufwindow.go +++ b/internal/display/bufwindow.go @@ -35,7 +35,8 @@ type BufWindow struct { func NewBufWindow(x, y, width, height int, buf *buffer.Buffer) *BufWindow { w := new(BufWindow) w.View = new(View) - w.X, w.Y, w.Width, w.Height, w.Buf = x, y, width, height, buf + w.X, w.Y, w.Width, w.Height = x, y, width, height + w.SetBuffer(buf) w.active = true w.sline = NewStatusLine(w) @@ -45,6 +46,16 @@ func NewBufWindow(x, y, width, height int, buf *buffer.Buffer) *BufWindow { func (w *BufWindow) SetBuffer(b *buffer.Buffer) { w.Buf = b + b.OptionCallback = func(option string, nativeValue interface{}) { + if option == "softwrap" { + if nativeValue.(bool) { + w.StartCol = 0 + } else { + w.StartLine.Row = 0 + } + w.Relocate() + } + } } func (w *BufWindow) GetView() *View { From cd7ab640c5b828dde0ba7b1b0196f5a78cc4c3f6 Mon Sep 17 00:00:00 2001 From: Dmitry Maluka Date: Wed, 17 Mar 2021 20:13:25 +0100 Subject: [PATCH 03/11] Fix displaying incomplete tab or wide rune at the right edge of window Fix displaying tabs and wide runes which don't fit in the window. Don't overwrite the vertical divider and the adjacent window. - For tabs: display only as many of the tab's spaces as fit in the window. - For wide runes: if a rune doesn't fit, don't display it in this line at all. If softwrap is on, display this rune in the next line. Fixes #1979 --- internal/display/bufwindow.go | 98 ++++++++++++++++++++++++----------- internal/display/softwrap.go | 51 ++++++++++++++++-- 2 files changed, 114 insertions(+), 35 deletions(-) diff --git a/internal/display/bufwindow.go b/internal/display/bufwindow.go index 0a9e174d..87bf27a5 100644 --- a/internal/display/bufwindow.go +++ b/internal/display/bufwindow.go @@ -261,40 +261,54 @@ func (w *BufWindow) LocFromVisual(svloc buffer.Loc) buffer.Loc { totalwidth := w.StartCol - nColsBeforeStart - if svloc.X <= vloc.X+w.X && vloc.Y+w.Y == svloc.Y { - return bloc - } for len(line) > 0 { - if vloc.X+w.X == svloc.X && vloc.Y+w.Y == svloc.Y { - return bloc - } - r, _, size := util.DecodeCharacter(line) - draw() + width := 0 switch r { case '\t': ts := tabsize - (totalwidth % tabsize) - width = ts + width = util.Min(ts, maxWidth-vloc.X) + totalwidth += ts default: width = runewidth.RuneWidth(r) + totalwidth += width } + // If a wide rune does not fit in the window + if vloc.X+width > maxWidth && vloc.X > w.gutterOffset { + if vloc.Y+w.Y == svloc.Y { + return bloc + } + + // We either stop or we wrap to draw the rune in the next line + if !softwrap { + break + } else { + vloc.Y++ + if vloc.Y >= w.bufHeight { + break + } + vloc.X = w.gutterOffset + } + } + + draw() + // Draw any extra characters either spaces for tabs or @ for incomplete wide runes if width > 1 { for i := 1; i < width; i++ { - if vloc.X+w.X == svloc.X && vloc.Y+w.Y == svloc.Y { - return bloc - } draw() } } + + if svloc.X < vloc.X+w.X && vloc.Y+w.Y == svloc.Y { + return bloc + } bloc.X++ line = line[size:] - totalwidth += width - // If we reach the end of the window then we either stop or we wrap for softwrap if vloc.X >= maxWidth { if !softwrap { @@ -623,26 +637,61 @@ func (w *BufWindow) displayBuffer() { nColsBeforeStart-- } + wrap := func() { + vloc.X = 0 + if w.hasMessage { + w.drawGutter(&vloc, &bloc) + } + if b.Settings["diffgutter"].(bool) { + w.drawDiffGutter(lineNumStyle, true, &vloc, &bloc) + } + + // This will draw an empty line number because the current line is wrapped + if b.Settings["ruler"].(bool) { + w.drawLineNum(lineNumStyle, true, &vloc, &bloc) + } + } + totalwidth := w.StartCol - nColsBeforeStart for len(line) > 0 { r, combc, size := util.DecodeCharacter(line) curStyle, _ = w.getStyle(curStyle, bloc) - draw(r, combc, curStyle, true) - width := 0 char := ' ' switch r { case '\t': ts := tabsize - (totalwidth % tabsize) - width = ts + width = util.Min(ts, maxWidth-vloc.X) + 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 { + for vloc.X < maxWidth { + draw(' ', nil, config.DefStyle, false) + } + + // We either stop or we wrap to draw the rune in the next line + if !softwrap { + break + } else { + vloc.Y++ + if vloc.Y >= w.bufHeight { + break + } + wrap() + } + } + + draw(r, combc, curStyle, true) + // Draw any extra characters either spaces for tabs or @ for incomplete wide runes if width > 1 { for i := 1; i < width; i++ { @@ -652,8 +701,6 @@ func (w *BufWindow) displayBuffer() { bloc.X++ line = line[size:] - totalwidth += width - // If we reach the end of the window then we either stop or we wrap for softwrap if vloc.X >= maxWidth { if !softwrap { @@ -663,18 +710,7 @@ func (w *BufWindow) displayBuffer() { if vloc.Y >= w.bufHeight { break } - vloc.X = 0 - if w.hasMessage { - w.drawGutter(&vloc, &bloc) - } - if b.Settings["diffgutter"].(bool) { - w.drawDiffGutter(lineNumStyle, true, &vloc, &bloc) - } - - // This will draw an empty line number because the current line is wrapped - if b.Settings["ruler"].(bool) { - w.drawLineNum(lineNumStyle, true, &vloc, &bloc) - } + wrap() } } } diff --git a/internal/display/softwrap.go b/internal/display/softwrap.go index bbcc99e0..fa0f92c8 100644 --- a/internal/display/softwrap.go +++ b/internal/display/softwrap.go @@ -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" ) @@ -36,13 +37,55 @@ type SoftWrap interface { } func (w *BufWindow) getRow(loc buffer.Loc) int { + if loc.X <= 0 { + return 0 + } + if w.bufWidth <= 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 / w.bufWidth + + tabsize := util.IntOpt(w.Buf.Settings["tabsize"]) + + line := w.Buf.LineBytes(loc.Y) + x := 0 + visualx := 0 + row := 0 + totalwidth := 0 + + for len(line) > 0 { + r, _, size := util.DecodeCharacter(line) + + width := 0 + switch r { + case '\t': + ts := tabsize - (totalwidth % tabsize) + width = util.Min(ts, w.bufWidth-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 + } + + if x == loc.X { + return row + } + x++ + line = line[size:] + + visualx += width + if visualx >= w.bufWidth { + row++ + visualx = 0 + } + } + return row } func (w *BufWindow) getRowCount(line int) int { From 0487db8b993bafa0a7a780a7872f52796de9b619 Mon Sep 17 00:00:00 2001 From: Dmitry Maluka Date: Wed, 3 Mar 2021 20:09:35 +0100 Subject: [PATCH 04/11] Fix horizontal scrolling with a wide rune at the right edge of window --- internal/display/bufwindow.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/internal/display/bufwindow.go b/internal/display/bufwindow.go index 87bf27a5..e493547b 100644 --- a/internal/display/bufwindow.go +++ b/internal/display/bufwindow.go @@ -208,12 +208,17 @@ func (w *BufWindow) Relocate() bool { // horizontal relocation (scrolling) if !b.Settings["softwrap"].(bool) { cx := activeC.GetVisualX() + rw := runewidth.RuneWidth(activeC.RuneUnder(activeC.X)) + if rw == 0 { + rw = 1 // tab or newline + } + if cx < w.StartCol { w.StartCol = cx ret = true } - if cx+w.gutterOffset+1 > w.StartCol+w.Width { - w.StartCol = cx - w.Width + w.gutterOffset + 1 + if cx+w.gutterOffset+rw > w.StartCol+w.Width { + w.StartCol = cx - w.Width + w.gutterOffset + rw ret = true } } From 7a3d1e6e300b6c5fec650f7a332fb7afdc3b15c5 Mon Sep 17 00:00:00 2001 From: Dmitry Maluka Date: Wed, 17 Mar 2021 21:16:52 +0100 Subject: [PATCH 05/11] Add VLoc, VLocFromLoc and LocFromVLoc VLoc allows any location in the buffer to be represented as a visual location in the linewrapped buffer. In particular, this is useful for implementing moving cursor up and down within a wrapped line. --- internal/display/infowindow.go | 8 ++- internal/display/softwrap.go | 120 ++++++++++++++++++++++++++++----- 2 files changed, 108 insertions(+), 20 deletions(-) diff --git a/internal/display/infowindow.go b/internal/display/infowindow.go index 530dce67..1892de39 100644 --- a/internal/display/infowindow.go +++ b/internal/display/infowindow.go @@ -75,9 +75,11 @@ func (i *InfoWindow) LocFromVisual(vloc buffer.Loc) buffer.Loc { func (i *InfoWindow) BufWidth() int { return i.Width } func (i *InfoWindow) BufHeight() int { return 1 } -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) 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) VLocFromLoc(loc buffer.Loc) VLoc { return VLoc{SLoc{0, 0}, loc.X} } +func (i *InfoWindow) LocFromVLoc(vloc VLoc) buffer.Loc { return buffer.Loc{vloc.VisualX, 0} } func (i *InfoWindow) Clear() { for x := 0; x < i.Width; x++ { diff --git a/internal/display/softwrap.go b/internal/display/softwrap.go index fa0f92c8..2cbfe175 100644 --- a/internal/display/softwrap.go +++ b/internal/display/softwrap.go @@ -30,27 +30,36 @@ 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 } tabsize := util.IntOpt(w.Buf.Settings["tabsize"]) line := w.Buf.LineBytes(loc.Y) x := 0 - visualx := 0 - row := 0 totalwidth := 0 for len(line) > 0 { @@ -60,7 +69,7 @@ func (w *BufWindow) getRow(loc buffer.Loc) int { 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) @@ -68,28 +77,81 @@ func (w *BufWindow) getRow(loc buffer.Loc) int { } // If a wide rune does not fit in the window - if visualx+width > w.bufWidth && visualx > 0 { - row++ - visualx = 0 + if vloc.VisualX+width > w.bufWidth && vloc.VisualX > 0 { + vloc.Row++ + vloc.VisualX = 0 } if x == loc.X { - return row + return vloc } x++ line = line[size:] - visualx += width - if visualx >= w.bufWidth { - row++ - visualx = 0 + vloc.VisualX += width + if vloc.VisualX >= w.bufWidth { + vloc.Row++ + vloc.VisualX = 0 } } - return row + return vloc +} + +func (w *BufWindow) getLocFromVLoc(svloc VLoc) buffer.Loc { + loc := buffer.Loc{X: 0, Y: svloc.Line} + + if w.bufWidth <= 0 { + return loc + } + + tabsize := util.IntOpt(w.Buf.Settings["tabsize"]) + + line := w.Buf.LineBytes(svloc.Line) + vloc := VLoc{SLoc: SLoc{svloc.Line, 0}, VisualX: 0} + + totalwidth := 0 + + for len(line) > 0 { + r, _, size := util.DecodeCharacter(line) + + 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 + } + + // If a wide rune does not fit in the window + if vloc.VisualX+width > w.bufWidth && vloc.VisualX > 0 { + if vloc.Row == svloc.Row { + return loc + } + vloc.Row++ + vloc.VisualX = 0 + } + + vloc.VisualX += width + if vloc.Row == svloc.Row && vloc.VisualX > svloc.VisualX { + return loc + } + loc.X++ + line = line[size:] + + 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 { @@ -184,5 +246,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) } From 6d13710d934dc1bfd804246c255bdce3e42fdc95 Mon Sep 17 00:00:00 2001 From: Dmitry Maluka Date: Sat, 6 Mar 2021 23:43:36 +0100 Subject: [PATCH 06/11] Implement moving cursor up/down within a wrapped line Modified behavior of CursorUp, CursorDown, CursorPageUp etc: if softwrap is enabled, cursor moves by visual lines, not logical lines. TODO: implement it also for Home and End keys: move cursor to the visual start or end of a line. I haven't implemented it for now, because I'm not sure what should be the behavior of StartOfTextToggle then (considering that Home key is bound to StartOfTextToggle by default). Fixes #1598 --- internal/action/actions.go | 55 ++++++++++++++++++++++++++++++----- internal/buffer/buffer.go | 6 ++++ internal/buffer/cursor.go | 4 +++ internal/display/bufwindow.go | 15 ++++++++++ 4 files changed, 72 insertions(+), 8 deletions(-) diff --git a/internal/action/actions.go b/internal/action/actions.go index c0334a6a..f25ad04e 100644 --- a/internal/action/actions.go +++ b/internal/action/actions.go @@ -123,10 +123,49 @@ func (h *BufPane) Center() bool { return true } +// MoveCursorUp is not an action +func (h *BufPane) MoveCursorUp(n int) { + if !h.Buf.Settings["softwrap"].(bool) { + h.Cursor.UpN(n) + } else { + vloc := h.VLocFromLoc(h.Cursor.Loc) + sloc := h.Scroll(vloc.SLoc, -n) + if sloc == vloc.SLoc { + // we are at the beginning of buffer + h.Cursor.Loc = h.Buf.Start() + h.Cursor.LastVisualX = 0 + } else { + vloc.SLoc = sloc + vloc.VisualX = h.Cursor.LastVisualX + h.Cursor.Loc = h.LocFromVLoc(vloc) + } + } +} + +// MoveCursorDown is not an action +func (h *BufPane) MoveCursorDown(n int) { + if !h.Buf.Settings["softwrap"].(bool) { + h.Cursor.DownN(n) + } else { + vloc := h.VLocFromLoc(h.Cursor.Loc) + sloc := h.Scroll(vloc.SLoc, n) + if sloc == vloc.SLoc { + // we are at the end of buffer + h.Cursor.Loc = h.Buf.End() + vloc = h.VLocFromLoc(h.Cursor.Loc) + h.Cursor.LastVisualX = vloc.VisualX + } else { + vloc.SLoc = sloc + vloc.VisualX = h.Cursor.LastVisualX + h.Cursor.Loc = h.LocFromVLoc(vloc) + } + } +} + // CursorUp moves the cursor up func (h *BufPane) CursorUp() bool { h.Cursor.Deselect(true) - h.Cursor.Up() + h.MoveCursorUp(1) h.Relocate() return true } @@ -134,7 +173,7 @@ func (h *BufPane) CursorUp() bool { // CursorDown moves the cursor down func (h *BufPane) CursorDown() bool { h.Cursor.Deselect(true) - h.Cursor.Down() + h.MoveCursorDown(1) h.Relocate() return true } @@ -212,7 +251,7 @@ func (h *BufPane) SelectUp() bool { if !h.Cursor.HasSelection() { h.Cursor.OrigSelection[0] = h.Cursor.Loc } - h.Cursor.Up() + h.MoveCursorUp(1) h.Cursor.SelectTo(h.Cursor.Loc) h.Relocate() return true @@ -223,7 +262,7 @@ func (h *BufPane) SelectDown() bool { if !h.Cursor.HasSelection() { h.Cursor.OrigSelection[0] = h.Cursor.Loc } - h.Cursor.Down() + h.MoveCursorDown(1) h.Cursor.SelectTo(h.Cursor.Loc) h.Relocate() return true @@ -1274,7 +1313,7 @@ func (h *BufPane) SelectPageUp() bool { if !h.Cursor.HasSelection() { h.Cursor.OrigSelection[0] = h.Cursor.Loc } - h.Cursor.UpN(h.BufHeight()) + h.MoveCursorUp(h.BufHeight()) h.Cursor.SelectTo(h.Cursor.Loc) h.Relocate() return true @@ -1285,7 +1324,7 @@ func (h *BufPane) SelectPageDown() bool { if !h.Cursor.HasSelection() { h.Cursor.OrigSelection[0] = h.Cursor.Loc } - h.Cursor.DownN(h.BufHeight()) + h.MoveCursorDown(h.BufHeight()) h.Cursor.SelectTo(h.Cursor.Loc) h.Relocate() return true @@ -1300,7 +1339,7 @@ func (h *BufPane) CursorPageUp() bool { h.Cursor.ResetSelection() h.Cursor.StoreVisualX() } - h.Cursor.UpN(h.BufHeight()) + h.MoveCursorUp(h.BufHeight()) h.Relocate() return true } @@ -1314,7 +1353,7 @@ func (h *BufPane) CursorPageDown() bool { h.Cursor.ResetSelection() h.Cursor.StoreVisualX() } - h.Cursor.DownN(h.BufHeight()) + h.MoveCursorDown(h.BufHeight()) h.Relocate() return true } diff --git a/internal/buffer/buffer.go b/internal/buffer/buffer.go index 718510dd..034e28e5 100644 --- a/internal/buffer/buffer.go +++ b/internal/buffer/buffer.go @@ -196,6 +196,12 @@ type Buffer struct { // the buffer module cannot directly call the display's API (it would mean // a circular dependency between packages). OptionCallback func(option string, nativeValue interface{}) + + // The display module registers its own GetVisualX function for getting + // the correct visual x location of a cursor when softwrap is used. + // This is hacky. Maybe it would be better to move all the visual x logic + // from buffer to display, but it would require rewriting a lot of code. + GetVisualX func(loc Loc) int } // NewBufferFromFileAtLoc opens a new buffer with a given cursor location diff --git a/internal/buffer/cursor.go b/internal/buffer/cursor.go index aa3daf02..12fc5db2 100644 --- a/internal/buffer/cursor.go +++ b/internal/buffer/cursor.go @@ -67,6 +67,10 @@ func (c *Cursor) GotoLoc(l Loc) { // GetVisualX returns the x value of the cursor in visual spaces func (c *Cursor) GetVisualX() int { + if c.buf.GetVisualX != nil { + return c.buf.GetVisualX(c.Loc) + } + if c.X <= 0 { c.X = 0 return 0 diff --git a/internal/display/bufwindow.go b/internal/display/bufwindow.go index e493547b..930d9c36 100644 --- a/internal/display/bufwindow.go +++ b/internal/display/bufwindow.go @@ -54,8 +54,15 @@ func (w *BufWindow) SetBuffer(b *buffer.Buffer) { w.StartLine.Row = 0 } w.Relocate() + + for _, c := range w.Buf.GetCursors() { + c.LastVisualX = c.GetVisualX() + } } } + b.GetVisualX = func(loc buffer.Loc) int { + return w.VLocFromLoc(loc).VisualX + } } func (w *BufWindow) GetView() *View { @@ -68,7 +75,15 @@ func (w *BufWindow) SetView(view *View) { func (w *BufWindow) Resize(width, height int) { w.Width, w.Height = width, height + w.updateDisplayInfo() + w.Relocate() + + if w.Buf.Settings["softwrap"].(bool) { + for _, c := range w.Buf.GetCursors() { + c.LastVisualX = c.GetVisualX() + } + } } func (w *BufWindow) SetActive(b bool) { From f2613eeb3b069b2905d71f0c59915600243d65ea Mon Sep 17 00:00:00 2001 From: Dmitry Maluka Date: Sun, 7 Mar 2021 14:56:00 +0100 Subject: [PATCH 07/11] Simplify LocFromVisual implementation Now that we have LocFromVLoc, we can radically simplify the code of LocFromVisual. Less duplication, less potential bugs. --- internal/display/bufwindow.go | 115 +++------------------------------- 1 file changed, 7 insertions(+), 108 deletions(-) diff --git a/internal/display/bufwindow.go b/internal/display/bufwindow.go index 930d9c36..5f95054f 100644 --- a/internal/display/bufwindow.go +++ b/internal/display/bufwindow.go @@ -242,119 +242,18 @@ func (w *BufWindow) Relocate() bool { // LocFromVisual takes a visual location (x and y position) and returns the // position in the buffer corresponding to the visual location -// Computing the buffer location requires essentially drawing the entire screen -// to account for complications like softwrap, wide characters, and horizontal scrolling // If the requested position does not correspond to a buffer location it returns // the nearest position func (w *BufWindow) LocFromVisual(svloc buffer.Loc) buffer.Loc { - b := w.Buf - - maxWidth := w.gutterOffset + w.bufWidth - - tabsize := int(b.Settings["tabsize"].(float64)) - softwrap := b.Settings["softwrap"].(bool) - - // 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 + vx := svloc.X - w.X - w.gutterOffset + if vx < 0 { + vx = 0 } - - // this represents the current draw position in the buffer (char positions) - bloc := buffer.Loc{X: -1, Y: w.StartLine.Line} - - for ; vloc.Y < w.bufHeight; vloc.Y++ { - vloc.X = w.gutterOffset - - line := b.LineBytes(bloc.Y) - line, nColsBeforeStart, bslice := util.SliceVisualEnd(line, w.StartCol, tabsize) - bloc.X = bslice - - draw := func() { - if nColsBeforeStart <= 0 { - vloc.X++ - } - nColsBeforeStart-- - } - - totalwidth := w.StartCol - nColsBeforeStart - - for len(line) > 0 { - r, _, size := util.DecodeCharacter(line) - - width := 0 - - switch r { - case '\t': - ts := tabsize - (totalwidth % tabsize) - width = util.Min(ts, maxWidth-vloc.X) - totalwidth += ts - default: - width = runewidth.RuneWidth(r) - totalwidth += width - } - - // If a wide rune does not fit in the window - if vloc.X+width > maxWidth && vloc.X > w.gutterOffset { - if vloc.Y+w.Y == svloc.Y { - return bloc - } - - // We either stop or we wrap to draw the rune in the next line - if !softwrap { - break - } else { - vloc.Y++ - if vloc.Y >= w.bufHeight { - break - } - vloc.X = w.gutterOffset - } - } - - draw() - - // Draw any extra characters either spaces for tabs or @ for incomplete wide runes - if width > 1 { - for i := 1; i < width; i++ { - draw() - } - } - - if svloc.X < vloc.X+w.X && vloc.Y+w.Y == svloc.Y { - return bloc - } - bloc.X++ - line = line[size:] - - // If we reach the end of the window then we either stop or we wrap for softwrap - if vloc.X >= maxWidth { - if !softwrap { - break - } else { - vloc.Y++ - if vloc.Y >= w.bufHeight { - break - } - vloc.X = w.gutterOffset - } - } - } - if vloc.Y+w.Y == svloc.Y { - return bloc - } - - if bloc.Y+1 >= b.LinesNum() || vloc.Y+1 >= w.bufHeight { - return bloc - } - - bloc.X = w.StartCol - bloc.Y++ + vloc := VLoc{ + SLoc: w.Scroll(w.StartLine, svloc.Y-w.Y), + VisualX: vx + w.StartCol, } - - return buffer.Loc{} + return w.LocFromVLoc(vloc) } func (w *BufWindow) drawGutter(vloc *buffer.Loc, bloc *buffer.Loc) { From 965e43ebf1319947a325f252000985fb0a586449 Mon Sep 17 00:00:00 2001 From: Dmitry Maluka Date: Wed, 17 Mar 2021 22:34:30 +0100 Subject: [PATCH 08/11] 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 From ab6ce444a74da75d7aa8d50583120e0d9294cea1 Mon Sep 17 00:00:00 2001 From: Dmitry Maluka Date: Wed, 17 Mar 2021 23:25:15 +0100 Subject: [PATCH 09/11] Don't highlight padding spaces in word wrapping Don't highlight space characters at the right edge which are used just for padding after line break in word wrapping, i.e. don't correspond to any real characters in the buffer. This makes it look nicer e.g. when selecting word-wrapped text. --- internal/display/bufwindow.go | 110 +++++++++++++++++----------------- 1 file changed, 56 insertions(+), 54 deletions(-) diff --git a/internal/display/bufwindow.go b/internal/display/bufwindow.go index 4ccd8d8d..bd8377d6 100644 --- a/internal/display/bufwindow.go +++ b/internal/display/bufwindow.go @@ -477,68 +477,70 @@ func (w *BufWindow) displayBuffer() { } bloc.X = bslice - draw := func(r rune, combc []rune, style tcell.Style, showcursor bool) { + draw := func(r rune, combc []rune, style tcell.Style, highlight bool, showcursor bool) { if nColsBeforeStart <= 0 && vloc.Y >= 0 { - _, origBg, _ := style.Decompose() - _, defBg, _ := config.DefStyle.Decompose() + if highlight { + _, origBg, _ := style.Decompose() + _, defBg, _ := config.DefStyle.Decompose() - // syntax highlighting with non-default background takes precedence - // over cursor-line and color-column - dontOverrideBackground := origBg != defBg + // syntax highlighting with non-default background takes precedence + // over cursor-line and color-column + dontOverrideBackground := origBg != defBg - for _, c := range cursors { - if c.HasSelection() && - (bloc.GreaterEqual(c.CurSelection[0]) && bloc.LessThan(c.CurSelection[1]) || - bloc.LessThan(c.CurSelection[0]) && bloc.GreaterEqual(c.CurSelection[1])) { - // The current character is selected - style = config.DefStyle.Reverse(true) + for _, c := range cursors { + if c.HasSelection() && + (bloc.GreaterEqual(c.CurSelection[0]) && bloc.LessThan(c.CurSelection[1]) || + bloc.LessThan(c.CurSelection[0]) && bloc.GreaterEqual(c.CurSelection[1])) { + // The current character is selected + style = config.DefStyle.Reverse(true) - if s, ok := config.Colorscheme["selection"]; ok { - style = s + if s, ok := config.Colorscheme["selection"]; ok { + style = s + } + } + + if b.Settings["cursorline"].(bool) && w.active && !dontOverrideBackground && + !c.HasSelection() && c.Y == bloc.Y { + if s, ok := config.Colorscheme["cursor-line"]; ok { + fg, _, _ := s.Decompose() + style = style.Background(fg) + } } } - if b.Settings["cursorline"].(bool) && w.active && !dontOverrideBackground && - !c.HasSelection() && c.Y == bloc.Y { - if s, ok := config.Colorscheme["cursor-line"]; ok { + for _, m := range b.Messages { + if bloc.GreaterEqual(m.Start) && bloc.LessThan(m.End) || + bloc.LessThan(m.End) && bloc.GreaterEqual(m.Start) { + style = style.Underline(true) + break + } + } + + if r == '\t' { + indentrunes := []rune(b.Settings["indentchar"].(string)) + // if empty indentchar settings, use space + if len(indentrunes) == 0 { + indentrunes = []rune{' '} + } + + r = indentrunes[0] + if s, ok := config.Colorscheme["indent-char"]; ok && r != ' ' { + fg, _, _ := s.Decompose() + style = style.Foreground(fg) + } + } + + if s, ok := config.Colorscheme["color-column"]; ok { + if colorcolumn != 0 && vloc.X-w.gutterOffset+w.StartCol == colorcolumn && !dontOverrideBackground { fg, _, _ := s.Decompose() style = style.Background(fg) } } - } - for _, m := range b.Messages { - if bloc.GreaterEqual(m.Start) && bloc.LessThan(m.End) || - bloc.LessThan(m.End) && bloc.GreaterEqual(m.Start) { - style = style.Underline(true) - break - } - } - - if r == '\t' { - indentrunes := []rune(b.Settings["indentchar"].(string)) - // if empty indentchar settings, use space - if len(indentrunes) == 0 { - indentrunes = []rune{' '} - } - - r = indentrunes[0] - if s, ok := config.Colorscheme["indent-char"]; ok && r != ' ' { - fg, _, _ := s.Decompose() - style = style.Foreground(fg) - } - } - - if s, ok := config.Colorscheme["color-column"]; ok { - if colorcolumn != 0 && vloc.X-w.gutterOffset+w.StartCol == colorcolumn && !dontOverrideBackground { - fg, _, _ := s.Decompose() - style = style.Background(fg) - } - } - - for _, mb := range matchingBraces { - if mb.X == bloc.X && mb.Y == bloc.Y { - style = style.Underline(true) + for _, mb := range matchingBraces { + if mb.X == bloc.X && mb.Y == bloc.Y { + style = style.Underline(true) + } } } @@ -622,7 +624,7 @@ func (w *BufWindow) displayBuffer() { // 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) + draw(' ', nil, config.DefStyle, false, false) } // We either stop or we wrap to draw the word in the next line @@ -638,7 +640,7 @@ func (w *BufWindow) displayBuffer() { } for _, r := range word { - draw(r.r, r.combc, r.style, true) + draw(r.r, r.combc, r.style, true, true) // Draw any extra characters either spaces for tabs or @ for incomplete wide runes if r.width > 1 { @@ -648,7 +650,7 @@ func (w *BufWindow) displayBuffer() { } for i := 1; i < r.width; i++ { - draw(char, nil, r.style, false) + draw(char, nil, r.style, true, false) } } bloc.X++ @@ -694,7 +696,7 @@ func (w *BufWindow) displayBuffer() { if vloc.X != maxWidth { // Display newline within a selection - draw(' ', nil, config.DefStyle, true) + draw(' ', nil, config.DefStyle, true, true) } bloc.X = w.StartCol From aaac60a78ded89332b8c20f3a54ee632e1f7eb1e Mon Sep 17 00:00:00 2001 From: Dmitry Maluka Date: Thu, 8 Apr 2021 23:32:00 +0200 Subject: [PATCH 10/11] Replace BufWidth & BufHeight with BufView BufView returns not only the buffer's width and height but also its x,y position. It may be useful e.g. for checking if a mouse click was on the actual buffer or ourside it, e.g. on the gutter. --- internal/action/actions.go | 24 ++++++++++++------------ internal/display/bufwindow.go | 22 ++++++++++++---------- internal/display/infowindow.go | 12 ++++++++++-- internal/display/window.go | 3 +-- 4 files changed, 35 insertions(+), 26 deletions(-) diff --git a/internal/action/actions.go b/internal/action/actions.go index f25ad04e..beb63b07 100644 --- a/internal/action/actions.go +++ b/internal/action/actions.go @@ -36,8 +36,8 @@ func (h *BufPane) ScrollDown(n int) { func (h *BufPane) ScrollAdjust() { v := h.GetView() end := h.SLocFromLoc(h.Buf.End()) - if h.Diff(v.StartLine, end) < h.BufHeight()-1 { - v.StartLine = h.Scroll(end, -h.BufHeight()+1) + if h.Diff(v.StartLine, end) < h.BufView().Height-1 { + v.StartLine = h.Scroll(end, -h.BufView().Height+1) } h.SetView(v) } @@ -117,7 +117,7 @@ func (h *BufPane) ScrollDownAction() bool { // Center centers the view on the cursor func (h *BufPane) Center() bool { v := h.GetView() - v.StartLine = h.Scroll(h.SLocFromLoc(h.Cursor.Loc), -h.BufHeight()/2) + v.StartLine = h.Scroll(h.SLocFromLoc(h.Cursor.Loc), -h.BufView().Height/2) h.SetView(v) h.ScrollAdjust() return true @@ -1290,20 +1290,20 @@ func (h *BufPane) Start() bool { // End moves the viewport to the end of the buffer func (h *BufPane) End() bool { v := h.GetView() - v.StartLine = h.Scroll(h.SLocFromLoc(h.Buf.End()), -h.BufHeight()+1) + v.StartLine = h.Scroll(h.SLocFromLoc(h.Buf.End()), -h.BufView().Height+1) h.SetView(v) return true } // PageUp scrolls the view up a page func (h *BufPane) PageUp() bool { - h.ScrollUp(h.BufHeight()) + h.ScrollUp(h.BufView().Height) return true } // PageDown scrolls the view down a page func (h *BufPane) PageDown() bool { - h.ScrollDown(h.BufHeight()) + h.ScrollDown(h.BufView().Height) h.ScrollAdjust() return true } @@ -1313,7 +1313,7 @@ func (h *BufPane) SelectPageUp() bool { if !h.Cursor.HasSelection() { h.Cursor.OrigSelection[0] = h.Cursor.Loc } - h.MoveCursorUp(h.BufHeight()) + h.MoveCursorUp(h.BufView().Height) h.Cursor.SelectTo(h.Cursor.Loc) h.Relocate() return true @@ -1324,7 +1324,7 @@ func (h *BufPane) SelectPageDown() bool { if !h.Cursor.HasSelection() { h.Cursor.OrigSelection[0] = h.Cursor.Loc } - h.MoveCursorDown(h.BufHeight()) + h.MoveCursorDown(h.BufView().Height) h.Cursor.SelectTo(h.Cursor.Loc) h.Relocate() return true @@ -1339,7 +1339,7 @@ func (h *BufPane) CursorPageUp() bool { h.Cursor.ResetSelection() h.Cursor.StoreVisualX() } - h.MoveCursorUp(h.BufHeight()) + h.MoveCursorUp(h.BufView().Height) h.Relocate() return true } @@ -1353,20 +1353,20 @@ func (h *BufPane) CursorPageDown() bool { h.Cursor.ResetSelection() h.Cursor.StoreVisualX() } - h.MoveCursorDown(h.BufHeight()) + h.MoveCursorDown(h.BufView().Height) h.Relocate() return true } // HalfPageUp scrolls the view up half a page func (h *BufPane) HalfPageUp() bool { - h.ScrollUp(h.BufHeight() / 2) + h.ScrollUp(h.BufView().Height / 2) return true } // HalfPageDown scrolls the view down half a page func (h *BufPane) HalfPageDown() bool { - h.ScrollDown(h.BufHeight() / 2) + h.ScrollDown(h.BufView().Height / 2) h.ScrollAdjust() return true } diff --git a/internal/display/bufwindow.go b/internal/display/bufwindow.go index bd8377d6..5db6c5fc 100644 --- a/internal/display/bufwindow.go +++ b/internal/display/bufwindow.go @@ -94,16 +94,18 @@ func (w *BufWindow) IsActive() bool { return w.active } -// BufWidth returns the width of the actual buffer displayed in the window, -// which is usually less than the window width due to the gutter, ruler or scrollbar -func (w *BufWindow) BufWidth() int { - return w.bufWidth -} - -// BufHeight returns the height of the actual buffer displayed in the window, -// which is usually less than the window height due to the statusline -func (w *BufWindow) BufHeight() int { - return w.bufHeight +// BufView returns the width, height and x,y location of the actual buffer. +// It is not exactly the same as the whole window which also contains gutter, +// ruler, scrollbar and statusline. +func (w *BufWindow) BufView() View { + return View{ + X: w.gutterOffset, + Y: 0, + Width: w.bufWidth, + Height: w.bufHeight, + StartLine: w.StartLine, + StartCol: w.StartCol, + } } func (w *BufWindow) updateDisplayInfo() { diff --git a/internal/display/infowindow.go b/internal/display/infowindow.go index 1892de39..7d21faca 100644 --- a/internal/display/infowindow.go +++ b/internal/display/infowindow.go @@ -72,8 +72,16 @@ func (i *InfoWindow) LocFromVisual(vloc buffer.Loc) buffer.Loc { return buffer.Loc{c.GetCharPosInLine(l, vloc.X-n), 0} } -func (i *InfoWindow) BufWidth() int { return i.Width } -func (i *InfoWindow) BufHeight() int { return 1 } +func (i *InfoWindow) BufView() View { + return View{ + X: 0, + Y: 0, + Width: i.Width, + Height: 1, + StartLine: SLoc{0, 0}, + StartCol: 0, + } +} func (i *InfoWindow) Scroll(s SLoc, n int) SLoc { return s } func (i *InfoWindow) Diff(s1, s2 SLoc) int { return 0 } diff --git a/internal/display/window.go b/internal/display/window.go index eb2c09f4..a321cf4f 100644 --- a/internal/display/window.go +++ b/internal/display/window.go @@ -33,6 +33,5 @@ type BWindow interface { Window SoftWrap SetBuffer(b *buffer.Buffer) - BufWidth() int - BufHeight() int + BufView() View } From 88c95c8faef2ec560a39858e1b6084ac3ed9c47b Mon Sep 17 00:00:00 2001 From: Dmitry Maluka Date: Fri, 9 Apr 2021 01:48:58 +0200 Subject: [PATCH 11/11] Fix up X,Y values in BufView Let's return absolute X, Y values, rather than relative to the bufwindow. --- internal/display/bufwindow.go | 4 ++-- internal/display/infowindow.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/display/bufwindow.go b/internal/display/bufwindow.go index 5db6c5fc..6044fd32 100644 --- a/internal/display/bufwindow.go +++ b/internal/display/bufwindow.go @@ -99,8 +99,8 @@ func (w *BufWindow) IsActive() bool { // ruler, scrollbar and statusline. func (w *BufWindow) BufView() View { return View{ - X: w.gutterOffset, - Y: 0, + X: w.X + w.gutterOffset, + Y: w.Y, Width: w.bufWidth, Height: w.bufHeight, StartLine: w.StartLine, diff --git a/internal/display/infowindow.go b/internal/display/infowindow.go index 7d21faca..3931067d 100644 --- a/internal/display/infowindow.go +++ b/internal/display/infowindow.go @@ -75,7 +75,7 @@ func (i *InfoWindow) LocFromVisual(vloc buffer.Loc) buffer.Loc { func (i *InfoWindow) BufView() View { return View{ X: 0, - Y: 0, + Y: i.Y, Width: i.Width, Height: 1, StartLine: SLoc{0, 0},