6 runewidth "github.com/mattn/go-runewidth"
7 "github.com/zyedidia/micro/v2/internal/buffer"
8 "github.com/zyedidia/micro/v2/internal/config"
9 "github.com/zyedidia/micro/v2/internal/screen"
10 "github.com/zyedidia/micro/v2/internal/util"
11 "github.com/zyedidia/tcell/v2"
14 // The BufWindow provides a way of displaying a certain section
16 type BufWindow struct {
19 // Buffer being shown in this window
34 // NewBufWindow creates a new window at a location in the screen with a width and height
35 func NewBufWindow(x, y, width, height int, buf *buffer.Buffer) *BufWindow {
38 w.X, w.Y, w.Width, w.Height = x, y, width, height
42 w.sline = NewStatusLine(w)
47 func (w *BufWindow) SetBuffer(b *buffer.Buffer) {
49 b.OptionCallback = func(option string, nativeValue interface{}) {
50 if option == "softwrap" {
51 if nativeValue.(bool) {
61 func (w *BufWindow) GetView() *View {
65 func (w *BufWindow) SetView(view *View) {
69 func (w *BufWindow) Resize(width, height int) {
70 w.Width, w.Height = width, height
74 func (w *BufWindow) SetActive(b bool) {
78 func (w *BufWindow) IsActive() bool {
82 // BufWidth returns the width of the actual buffer displayed in the window,
83 // which is usually less than the window width due to the gutter, ruler or scrollbar
84 func (w *BufWindow) BufWidth() int {
88 // BufHeight returns the height of the actual buffer displayed in the window,
89 // which is usually less than the window height due to the statusline
90 func (w *BufWindow) BufHeight() int {
94 func (w *BufWindow) updateDisplayInfo() {
98 if !b.Settings["statusline"].(bool) {
99 _, h := screen.Screen.Size()
101 if config.GetGlobalOption("infobar").(bool) {
104 if w.Y+w.Height != infoY {
109 w.bufHeight = w.Height
110 if b.Settings["statusline"].(bool) || w.drawDivider {
114 w.hasMessage = len(b.Messages) > 0
116 // We need to know the string length of the largest line number
117 // so we can pad appropriately when displaying line numbers
118 w.maxLineNumLength = len(strconv.Itoa(b.LinesNum()))
124 if b.Settings["diffgutter"].(bool) {
127 if b.Settings["ruler"].(bool) {
128 w.gutterOffset += w.maxLineNumLength + 1
131 w.bufWidth = w.Width - w.gutterOffset
132 if w.Buf.Settings["scrollbar"].(bool) && w.Buf.LinesNum() > w.Height {
137 func (w *BufWindow) getStartInfo(n, lineN int) ([]byte, int, int, *tcell.Style) {
138 tabsize := util.IntOpt(w.Buf.Settings["tabsize"])
140 bloc := buffer.Loc{0, lineN}
141 b := w.Buf.LineBytes(lineN)
142 curStyle := config.DefStyle
145 r, _, size := util.DecodeCharacter(b)
147 curStyle, found := w.getStyle(curStyle, bloc)
155 ts := tabsize - (width % tabsize)
158 w = runewidth.RuneWidth(r)
161 return b, n - width, bloc.X, s
167 return b, n - width, bloc.X, s
170 // Clear resets all cells in this window to the default style
171 func (w *BufWindow) Clear() {
172 for y := 0; y < w.Height; y++ {
173 for x := 0; x < w.Width; x++ {
174 screen.SetContent(w.X+x, w.Y+y, ' ', nil, config.DefStyle)
179 // Relocate moves the view window so that the cursor is in view
180 // This is useful if the user has scrolled far away, and then starts typing
181 // Returns true if the window location is moved
182 func (w *BufWindow) Relocate() bool {
184 height := w.bufHeight
186 activeC := w.Buf.GetActiveCursor()
187 scrollmargin := int(b.Settings["scrollmargin"].(float64))
189 c := w.SLocFromLoc(activeC.Loc)
191 bEnd := w.SLocFromLoc(b.End())
193 if c.LessThan(w.Scroll(w.StartLine, scrollmargin)) && c.GreaterThan(w.Scroll(bStart, scrollmargin-1)) {
194 w.StartLine = w.Scroll(c, -scrollmargin)
196 } else if c.LessThan(w.StartLine) {
200 if c.GreaterThan(w.Scroll(w.StartLine, height-1-scrollmargin)) && c.LessThan(w.Scroll(bEnd, -scrollmargin+1)) {
201 w.StartLine = w.Scroll(c, -height+1+scrollmargin)
203 } else if c.GreaterThan(w.Scroll(bEnd, -scrollmargin)) && c.GreaterThan(w.Scroll(w.StartLine, height-1)) {
204 w.StartLine = w.Scroll(bEnd, -height+1)
208 // horizontal relocation (scrolling)
209 if !b.Settings["softwrap"].(bool) {
210 cx := activeC.GetVisualX()
211 rw := runewidth.RuneWidth(activeC.RuneUnder(activeC.X))
213 rw = 1 // tab or newline
220 if cx+w.gutterOffset+rw > w.StartCol+w.Width {
221 w.StartCol = cx - w.Width + w.gutterOffset + rw
228 // LocFromVisual takes a visual location (x and y position) and returns the
229 // position in the buffer corresponding to the visual location
230 // Computing the buffer location requires essentially drawing the entire screen
231 // to account for complications like softwrap, wide characters, and horizontal scrolling
232 // If the requested position does not correspond to a buffer location it returns
233 // the nearest position
234 func (w *BufWindow) LocFromVisual(svloc buffer.Loc) buffer.Loc {
237 maxWidth := w.gutterOffset + w.bufWidth
239 tabsize := int(b.Settings["tabsize"].(float64))
240 softwrap := b.Settings["softwrap"].(bool)
242 // this represents the current draw position
243 // within the current window
244 vloc := buffer.Loc{X: 0, Y: 0}
246 // the start line may be partially out of the current window
247 vloc.Y = -w.StartLine.Row
250 // this represents the current draw position in the buffer (char positions)
251 bloc := buffer.Loc{X: -1, Y: w.StartLine.Line}
253 for ; vloc.Y < w.bufHeight; vloc.Y++ {
254 vloc.X = w.gutterOffset
256 line := b.LineBytes(bloc.Y)
257 line, nColsBeforeStart, bslice := util.SliceVisualEnd(line, w.StartCol, tabsize)
261 if nColsBeforeStart <= 0 {
267 totalwidth := w.StartCol - nColsBeforeStart
270 r, _, size := util.DecodeCharacter(line)
276 ts := tabsize - (totalwidth % tabsize)
277 width = util.Min(ts, maxWidth-vloc.X)
280 width = runewidth.RuneWidth(r)
284 // If a wide rune does not fit in the window
285 if vloc.X+width > maxWidth && vloc.X > w.gutterOffset {
286 if vloc.Y+w.Y == svloc.Y {
290 // We either stop or we wrap to draw the rune in the next line
295 if vloc.Y >= w.bufHeight {
298 vloc.X = w.gutterOffset
304 // Draw any extra characters either spaces for tabs or @ for incomplete wide runes
306 for i := 1; i < width; i++ {
311 if svloc.X < vloc.X+w.X && vloc.Y+w.Y == svloc.Y {
317 // If we reach the end of the window then we either stop or we wrap for softwrap
318 if vloc.X >= maxWidth {
323 if vloc.Y >= w.bufHeight {
326 vloc.X = w.gutterOffset
330 if vloc.Y+w.Y == svloc.Y {
334 if bloc.Y+1 >= b.LinesNum() || vloc.Y+1 >= w.bufHeight {
345 func (w *BufWindow) drawGutter(vloc *buffer.Loc, bloc *buffer.Loc) {
348 for _, m := range w.Buf.Messages {
349 if m.Start.Y == bloc.Y || m.End.Y == bloc.Y {
355 screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, char, nil, s)
357 screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, char, nil, s)
361 func (w *BufWindow) drawDiffGutter(backgroundStyle tcell.Style, softwrapped bool, vloc *buffer.Loc, bloc *buffer.Loc) {
365 switch w.Buf.DiffStatus(bloc.Y) {
367 symbol = '\u258C' // Left half block
368 styleName = "diff-added"
369 case buffer.DSModified:
370 symbol = '\u258C' // Left half block
371 styleName = "diff-modified"
372 case buffer.DSDeletedAbove:
374 symbol = '\u2594' // Upper one eighth block
375 styleName = "diff-deleted"
379 style := backgroundStyle
380 if s, ok := config.Colorscheme[styleName]; ok {
381 foreground, _, _ := s.Decompose()
382 style = style.Foreground(foreground)
385 screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, symbol, nil, style)
389 func (w *BufWindow) drawLineNum(lineNumStyle tcell.Style, softwrapped bool, vloc *buffer.Loc, bloc *buffer.Loc) {
390 cursorLine := w.Buf.GetActiveCursor().Loc.Y
392 if w.Buf.Settings["relativeruler"] == false || cursorLine == bloc.Y {
395 lineInt = bloc.Y - cursorLine
397 lineNum := strconv.Itoa(util.Abs(lineInt))
399 // Write the spaces before the line number if necessary
400 for i := 0; i < w.maxLineNumLength-len(lineNum); i++ {
401 screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, ' ', nil, lineNumStyle)
404 // Write the actual line number
405 for _, ch := range lineNum {
407 screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, ' ', nil, lineNumStyle)
409 screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, ch, nil, lineNumStyle)
414 // Write the extra space
415 screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, ' ', nil, lineNumStyle)
419 // getStyle returns the highlight style for the given character position
420 // If there is no change to the current highlight style it just returns that
421 func (w *BufWindow) getStyle(style tcell.Style, bloc buffer.Loc) (tcell.Style, bool) {
422 if group, ok := w.Buf.Match(bloc.Y)[bloc.X]; ok {
423 s := config.GetColor(group.String())
429 func (w *BufWindow) showCursor(x, y int, main bool) {
432 screen.ShowCursor(x, y)
434 screen.ShowFakeCursorMulti(x, y)
439 // displayBuffer draws the buffer being shown in this window on the screen.Screen
440 func (w *BufWindow) displayBuffer() {
443 if w.Height <= 0 || w.Width <= 0 {
447 maxWidth := w.gutterOffset + w.bufWidth
449 if b.ModifiedThisFrame {
450 if b.Settings["diffgutter"].(bool) {
451 b.UpdateDiff(func(synchronous bool) {
452 // If the diff was updated asynchronously, the outer call to
453 // displayBuffer might already be completed and we need to
454 // schedule a redraw in order to display the new diff.
455 // Note that this cannot lead to an infinite recursion
456 // because the modifications were cleared above so there won't
457 // be another call to UpdateDiff when displayBuffer is called
458 // during the redraw.
464 b.ModifiedThisFrame = false
467 var matchingBraces []buffer.Loc
468 // bracePairs is defined in buffer.go
469 if b.Settings["matchbrace"].(bool) {
470 for _, bp := range buffer.BracePairs {
471 for _, c := range b.GetCursors() {
472 if c.HasSelection() {
478 r := c.RuneUnder(curX)
479 rl := c.RuneUnder(curX - 1)
480 if r == bp[0] || r == bp[1] || rl == bp[0] || rl == bp[1] {
481 mb, left, found := b.FindMatchingBrace(bp, curLoc)
483 matchingBraces = append(matchingBraces, mb)
485 matchingBraces = append(matchingBraces, curLoc)
487 matchingBraces = append(matchingBraces, curLoc.Move(-1, b))
495 lineNumStyle := config.DefStyle
496 if style, ok := config.Colorscheme["line-number"]; ok {
499 curNumStyle := config.DefStyle
500 if style, ok := config.Colorscheme["current-line-number"]; ok {
501 if !b.Settings["cursorline"].(bool) {
502 curNumStyle = lineNumStyle
508 softwrap := b.Settings["softwrap"].(bool)
509 tabsize := util.IntOpt(b.Settings["tabsize"])
510 colorcolumn := util.IntOpt(b.Settings["colorcolumn"])
512 // this represents the current draw position
513 // within the current window
514 vloc := buffer.Loc{X: 0, Y: 0}
516 // the start line may be partially out of the current window
517 vloc.Y = -w.StartLine.Row
520 // this represents the current draw position in the buffer (char positions)
521 bloc := buffer.Loc{X: -1, Y: w.StartLine.Line}
523 cursors := b.GetCursors()
525 curStyle := config.DefStyle
526 for ; vloc.Y < w.bufHeight; vloc.Y++ {
530 for _, c := range cursors {
531 if bloc.Y == c.Y && w.active {
544 w.drawGutter(&vloc, &bloc)
547 if b.Settings["diffgutter"].(bool) {
548 w.drawDiffGutter(s, false, &vloc, &bloc)
551 if b.Settings["ruler"].(bool) {
552 w.drawLineNum(s, false, &vloc, &bloc)
555 vloc.X = w.gutterOffset
558 line, nColsBeforeStart, bslice, startStyle := w.getStartInfo(w.StartCol, bloc.Y)
559 if startStyle != nil {
560 curStyle = *startStyle
564 draw := func(r rune, combc []rune, style tcell.Style, showcursor bool) {
565 if nColsBeforeStart <= 0 && vloc.Y >= 0 {
566 _, origBg, _ := style.Decompose()
567 _, defBg, _ := config.DefStyle.Decompose()
569 // syntax highlighting with non-default background takes precedence
570 // over cursor-line and color-column
571 dontOverrideBackground := origBg != defBg
573 for _, c := range cursors {
574 if c.HasSelection() &&
575 (bloc.GreaterEqual(c.CurSelection[0]) && bloc.LessThan(c.CurSelection[1]) ||
576 bloc.LessThan(c.CurSelection[0]) && bloc.GreaterEqual(c.CurSelection[1])) {
577 // The current character is selected
578 style = config.DefStyle.Reverse(true)
580 if s, ok := config.Colorscheme["selection"]; ok {
585 if b.Settings["cursorline"].(bool) && w.active && !dontOverrideBackground &&
586 !c.HasSelection() && c.Y == bloc.Y {
587 if s, ok := config.Colorscheme["cursor-line"]; ok {
588 fg, _, _ := s.Decompose()
589 style = style.Background(fg)
594 for _, m := range b.Messages {
595 if bloc.GreaterEqual(m.Start) && bloc.LessThan(m.End) ||
596 bloc.LessThan(m.End) && bloc.GreaterEqual(m.Start) {
597 style = style.Underline(true)
603 indentrunes := []rune(b.Settings["indentchar"].(string))
604 // if empty indentchar settings, use space
605 if len(indentrunes) == 0 {
606 indentrunes = []rune{' '}
610 if s, ok := config.Colorscheme["indent-char"]; ok && r != ' ' {
611 fg, _, _ := s.Decompose()
612 style = style.Foreground(fg)
616 if s, ok := config.Colorscheme["color-column"]; ok {
617 if colorcolumn != 0 && vloc.X-w.gutterOffset+w.StartCol == colorcolumn && !dontOverrideBackground {
618 fg, _, _ := s.Decompose()
619 style = style.Background(fg)
623 for _, mb := range matchingBraces {
624 if mb.X == bloc.X && mb.Y == bloc.Y {
625 style = style.Underline(true)
629 screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, r, combc, style)
632 for _, c := range cursors {
633 if c.X == bloc.X && c.Y == bloc.Y && !c.HasSelection() {
634 w.showCursor(w.X+vloc.X, w.Y+vloc.Y, c.Num == 0)
639 if nColsBeforeStart <= 0 {
648 w.drawGutter(&vloc, &bloc)
650 if b.Settings["diffgutter"].(bool) {
651 w.drawDiffGutter(lineNumStyle, true, &vloc, &bloc)
654 // This will draw an empty line number because the current line is wrapped
655 if b.Settings["ruler"].(bool) {
656 w.drawLineNum(lineNumStyle, true, &vloc, &bloc)
660 totalwidth := w.StartCol - nColsBeforeStart
662 r, combc, size := util.DecodeCharacter(line)
664 curStyle, _ = w.getStyle(curStyle, bloc)
671 ts := tabsize - (totalwidth % tabsize)
672 width = util.Min(ts, maxWidth-vloc.X)
675 width = runewidth.RuneWidth(r)
680 // If a wide rune does not fit in the window
681 if vloc.X+width > maxWidth && vloc.X > w.gutterOffset {
682 for vloc.X < maxWidth {
683 draw(' ', nil, config.DefStyle, false)
686 // We either stop or we wrap to draw the rune in the next line
691 if vloc.Y >= w.bufHeight {
698 draw(r, combc, curStyle, true)
700 // Draw any extra characters either spaces for tabs or @ for incomplete wide runes
702 for i := 1; i < width; i++ {
703 draw(char, nil, curStyle, false)
709 // If we reach the end of the window then we either stop or we wrap for softwrap
710 if vloc.X >= maxWidth {
715 if vloc.Y >= w.bufHeight {
723 style := config.DefStyle
724 for _, c := range cursors {
725 if b.Settings["cursorline"].(bool) && w.active &&
726 !c.HasSelection() && c.Y == bloc.Y {
727 if s, ok := config.Colorscheme["cursor-line"]; ok {
728 fg, _, _ := s.Decompose()
729 style = style.Background(fg)
733 for i := vloc.X; i < maxWidth; i++ {
735 if s, ok := config.Colorscheme["color-column"]; ok {
736 if colorcolumn != 0 && i-w.gutterOffset+w.StartCol == colorcolumn {
737 fg, _, _ := s.Decompose()
738 curStyle = style.Background(fg)
741 screen.SetContent(i+w.X, vloc.Y+w.Y, ' ', nil, curStyle)
744 if vloc.X != maxWidth {
745 // Display newline within a selection
746 draw(' ', nil, config.DefStyle, true)
751 if bloc.Y >= b.LinesNum() {
757 func (w *BufWindow) displayStatusLine() {
758 if w.Buf.Settings["statusline"].(bool) {
760 } else if w.drawDivider {
761 divchars := config.GetGlobalOption("divchars").(string)
762 if util.CharacterCountInString(divchars) != 2 {
766 _, _, size := util.DecodeCharacterInString(divchars)
767 divchar, combc, _ := util.DecodeCharacterInString(divchars[size:])
769 dividerStyle := config.DefStyle
770 if style, ok := config.Colorscheme["divider"]; ok {
774 divreverse := config.GetGlobalOption("divreverse").(bool)
776 dividerStyle = dividerStyle.Reverse(true)
779 for x := w.X; x < w.X+w.Width; x++ {
780 screen.SetContent(x, w.Y+w.Height-1, divchar, combc, dividerStyle)
785 func (w *BufWindow) displayScrollBar() {
786 if w.Buf.Settings["scrollbar"].(bool) && w.Buf.LinesNum() > w.Height {
787 scrollX := w.X + w.Width - 1
788 barsize := int(float64(w.Height) / float64(w.Buf.LinesNum()) * float64(w.Height))
792 barstart := w.Y + int(float64(w.StartLine.Line)/float64(w.Buf.LinesNum())*float64(w.Height))
794 scrollBarStyle := config.DefStyle.Reverse(true)
795 if style, ok := config.Colorscheme["scrollbar"]; ok {
796 scrollBarStyle = style
799 for y := barstart; y < util.Min(barstart+barsize, w.Y+w.bufHeight); y++ {
800 screen.SetContent(scrollX, y, '|', nil, scrollBarStyle)
805 // Display displays the buffer and the statusline
806 func (w *BufWindow) Display() {
807 w.updateDisplayInfo()
809 w.displayStatusLine()