diff --git a/internal/action/actions.go b/internal/action/actions.go index 6db8e822..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) < v.Height-1 { - v.StartLine = h.Scroll(end, -v.Height+1) + if h.Diff(v.StartLine, end) < h.BufView().Height-1 { + v.StartLine = h.Scroll(end, -h.BufView().Height+1) } h.SetView(v) } @@ -117,16 +117,55 @@ 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.BufView().Height/2) h.SetView(v) h.ScrollAdjust() 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 @@ -1251,22 +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()), -v.Height+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 { - v := h.GetView() - h.ScrollUp(v.Height) + h.ScrollUp(h.BufView().Height) return true } // PageDown scrolls the view down a page func (h *BufPane) PageDown() bool { - v := h.GetView() - h.ScrollDown(v.Height) + h.ScrollDown(h.BufView().Height) h.ScrollAdjust() return true } @@ -1276,7 +1313,7 @@ func (h *BufPane) SelectPageUp() bool { if !h.Cursor.HasSelection() { h.Cursor.OrigSelection[0] = h.Cursor.Loc } - h.Cursor.UpN(h.GetView().Height) + h.MoveCursorUp(h.BufView().Height) h.Cursor.SelectTo(h.Cursor.Loc) h.Relocate() return true @@ -1287,7 +1324,7 @@ func (h *BufPane) SelectPageDown() bool { if !h.Cursor.HasSelection() { h.Cursor.OrigSelection[0] = h.Cursor.Loc } - h.Cursor.DownN(h.GetView().Height) + h.MoveCursorDown(h.BufView().Height) h.Cursor.SelectTo(h.Cursor.Loc) h.Relocate() return true @@ -1302,7 +1339,7 @@ func (h *BufPane) CursorPageUp() bool { h.Cursor.ResetSelection() h.Cursor.StoreVisualX() } - h.Cursor.UpN(h.GetView().Height) + h.MoveCursorUp(h.BufView().Height) h.Relocate() return true } @@ -1316,22 +1353,20 @@ func (h *BufPane) CursorPageDown() bool { h.Cursor.ResetSelection() h.Cursor.StoreVisualX() } - h.Cursor.DownN(h.GetView().Height) + h.MoveCursorDown(h.BufView().Height) 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.BufView().Height / 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.BufView().Height / 2) h.ScrollAdjust() return true } diff --git a/internal/buffer/buffer.go b/internal/buffer/buffer.go index 84ab7dc0..034e28e5 100644 --- a/internal/buffer/buffer.go +++ b/internal/buffer/buffer.go @@ -189,6 +189,19 @@ 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{}) + + // 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/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/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 2fcf0ac5..6044fd32 100644 --- a/internal/display/bufwindow.go +++ b/internal/display/bufwindow.go @@ -23,15 +23,20 @@ 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 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) @@ -41,6 +46,23 @@ 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() + + 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 { @@ -53,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) { @@ -64,6 +94,63 @@ func (w *BufWindow) IsActive() bool { return w.active } +// 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.X + w.gutterOffset, + Y: w.Y, + Width: w.bufWidth, + Height: w.bufHeight, + StartLine: w.StartLine, + StartCol: w.StartCol, + } +} + +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 +198,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)) @@ -141,12 +225,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 } } @@ -155,127 +244,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 - - hasMessage := len(b.Messages) > 0 - bufHeight := w.Height - if w.drawStatus { - bufHeight-- + vx := svloc.X - w.X - w.gutterOffset + if vx < 0 { + vx = 0 } - - bufWidth := w.Width - if w.Buf.Settings["scrollbar"].(bool) && w.Buf.LinesNum() > w.Height { - bufWidth-- + vloc := VLoc{ + SLoc: w.Scroll(w.StartLine, svloc.Y-w.Y), + VisualX: vx + w.StartCol, } - - // 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())) - - 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 - } - - // 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 - } - - 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 - - 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 - default: - width = runewidth.RuneWidth(r) - } - - // 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() - } - } - 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 >= bufWidth { - if !softwrap { - break - } else { - vloc.Y++ - if vloc.Y >= bufHeight { - break - } - vloc.X = w.gutterOffset - } - } - } - if vloc.Y+w.Y == svloc.Y { - return bloc - } - - if bloc.Y+1 >= b.LinesNum() || vloc.Y+1 >= bufHeight { - return bloc - } - - bloc.X = w.StartCol - bloc.Y++ - } - - return buffer.Loc{} + return w.LocFromVLoc(vloc) } func (w *BufWindow) drawGutter(vloc *buffer.Loc, bloc *buffer.Loc) { @@ -322,7 +302,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 +313,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 +360,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,11 +421,9 @@ 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) + wordwrap := softwrap && b.Settings["wordwrap"].(bool) + tabsize := util.IntOpt(b.Settings["tabsize"]) colorcolumn := util.IntOpt(b.Settings["colorcolumn"]) @@ -472,7 +441,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 +458,7 @@ func (w *BufWindow) displayBuffer() { } if vloc.Y >= 0 { - if hasMessage { + if w.hasMessage { w.drawGutter(&vloc, &bloc) } @@ -498,90 +467,82 @@ 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 } 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) + } } } @@ -601,59 +562,116 @@ 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) + } + } + + 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) - - draw(r, combc, curStyle, true) + 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) - width = ts + width = util.Min(ts, maxWidth-vloc.X) + totalwidth += ts default: width = runewidth.RuneWidth(r) - char = '@' + totalwidth += width } - // 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) + 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 } } - bloc.X++ - line = line[size:] - totalwidth += width + // 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, false) + } - // If we reach the end of the window then we either stop or we wrap for softwrap - if vloc.X >= bufWidth { + // We either stop or we wrap to draw the word in the next line if !softwrap { break } else { vloc.Y++ - if vloc.Y >= bufHeight { + if vloc.Y >= w.bufHeight { break } - vloc.X = 0 - if hasMessage { - w.drawGutter(&vloc, &bloc) - } - if b.Settings["diffgutter"].(bool) { - w.drawDiffGutter(lineNumStyle, true, &vloc, &bloc) + wrap() + } + } + + for _, r := range word { + 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 { + char := ' ' + if r.r != '\t' { + char = '@' } - // 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) + for i := 1; i < r.width; i++ { + draw(char, nil, r.style, true, false) } } + bloc.X++ + } + + 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 { + if !softwrap { + break + } else { + vloc.Y++ + if vloc.Y >= w.bufHeight { + break + } + wrap() + } } } @@ -667,7 +685,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,9 +696,9 @@ 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) + draw(' ', nil, config.DefStyle, true, true) } bloc.X = w.StartCol @@ -692,18 +710,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 +734,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 +751,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 +759,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..3931067d 100644 --- a/internal/display/infowindow.go +++ b/internal/display/infowindow.go @@ -72,9 +72,22 @@ 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) BufView() View { + return View{ + X: 0, + Y: i.Y, + 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 } +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 0f99b526..0597f061 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" ) @@ -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 } - // 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 + + 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 + } + } + 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) } diff --git a/internal/display/window.go b/internal/display/window.go index eb71970f..a321cf4f 100644 --- a/internal/display/window.go +++ b/internal/display/window.go @@ -33,4 +33,5 @@ type BWindow interface { Window SoftWrap SetBuffer(b *buffer.Buffer) + BufView() View } 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