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 of a buffer.
15 type BufWindow struct {
18 // Buffer being shown in this window
33 // NewBufWindow creates a new window at a location in the screen with a width and height
34 func NewBufWindow(x, y, width, height int, buf *buffer.Buffer) *BufWindow {
37 w.X, w.Y, w.Width, w.Height = x, y, width, height
41 w.sline = NewStatusLine(w)
46 // SetBuffer sets this window's buffer.
47 func (w *BufWindow) SetBuffer(b *buffer.Buffer) {
49 b.OptionCallback = func(option string, nativeValue interface{}) {
50 if option == "softwrap" {
51 if nativeValue.(bool) {
58 for _, c := range w.Buf.GetCursors() {
59 c.LastVisualX = c.GetVisualX()
63 b.GetVisualX = func(loc buffer.Loc) int {
64 return w.VLocFromLoc(loc).VisualX
68 // GetView gets the view.
69 func (w *BufWindow) GetView() *View {
73 // GetView sets the view.
74 func (w *BufWindow) SetView(view *View) {
78 // Resize resizes this window.
79 func (w *BufWindow) Resize(width, height int) {
80 w.Width, w.Height = width, height
85 if w.Buf.Settings["softwrap"].(bool) {
86 for _, c := range w.Buf.GetCursors() {
87 c.LastVisualX = c.GetVisualX()
92 // SetActive marks the window as active.
93 func (w *BufWindow) SetActive(b bool) {
97 // IsActive returns true if this window is active.
98 func (w *BufWindow) IsActive() bool {
102 // BufView returns the width, height and x,y location of the actual buffer.
103 // It is not exactly the same as the whole window which also contains gutter,
104 // ruler, scrollbar and statusline.
105 func (w *BufWindow) BufView() View {
107 X: w.X + w.gutterOffset,
111 StartLine: w.StartLine,
112 StartCol: w.StartCol,
116 func (w *BufWindow) updateDisplayInfo() {
119 w.drawDivider = false
120 if !b.Settings["statusline"].(bool) {
121 _, h := screen.Screen.Size()
123 if config.GetGlobalOption("infobar").(bool) {
126 if w.Y+w.Height != infoY {
131 w.bufHeight = w.Height
132 if b.Settings["statusline"].(bool) || w.drawDivider {
136 w.hasMessage = len(b.Messages) > 0
138 // We need to know the string length of the largest line number
139 // so we can pad appropriately when displaying line numbers
140 w.maxLineNumLength = len(strconv.Itoa(b.LinesNum()))
146 if b.Settings["diffgutter"].(bool) {
149 if b.Settings["ruler"].(bool) {
150 w.gutterOffset += w.maxLineNumLength + 1
153 w.bufWidth = w.Width - w.gutterOffset
154 if w.Buf.Settings["scrollbar"].(bool) && w.Buf.LinesNum() > w.Height {
159 func (w *BufWindow) getStartInfo(n, lineN int) ([]byte, int, int, *tcell.Style) {
160 tabsize := util.IntOpt(w.Buf.Settings["tabsize"])
162 bloc := buffer.Loc{0, lineN}
163 b := w.Buf.LineBytes(lineN)
164 curStyle := config.DefStyle
167 r, _, size := util.DecodeCharacter(b)
169 curStyle, found := w.getStyle(curStyle, bloc)
177 ts := tabsize - (width % tabsize)
180 w = runewidth.RuneWidth(r)
183 return b, n - width, bloc.X, s
189 return b, n - width, bloc.X, s
192 // Clear resets all cells in this window to the default style
193 func (w *BufWindow) Clear() {
194 for y := 0; y < w.Height; y++ {
195 for x := 0; x < w.Width; x++ {
196 screen.SetContent(w.X+x, w.Y+y, ' ', nil, config.DefStyle)
201 // Relocate moves the view window so that the cursor is in view
202 // This is useful if the user has scrolled far away, and then starts typing
203 // Returns true if the window location is moved
204 func (w *BufWindow) Relocate() bool {
206 height := w.bufHeight
208 activeC := w.Buf.GetActiveCursor()
209 scrollmargin := int(b.Settings["scrollmargin"].(float64))
211 c := w.SLocFromLoc(activeC.Loc)
213 bEnd := w.SLocFromLoc(b.End())
215 if c.LessThan(w.Scroll(w.StartLine, scrollmargin)) && c.GreaterThan(w.Scroll(bStart, scrollmargin-1)) {
216 w.StartLine = w.Scroll(c, -scrollmargin)
218 } else if c.LessThan(w.StartLine) {
222 if c.GreaterThan(w.Scroll(w.StartLine, height-1-scrollmargin)) && c.LessThan(w.Scroll(bEnd, -scrollmargin+1)) {
223 w.StartLine = w.Scroll(c, -height+1+scrollmargin)
225 } else if c.GreaterThan(w.Scroll(bEnd, -scrollmargin)) && c.GreaterThan(w.Scroll(w.StartLine, height-1)) {
226 w.StartLine = w.Scroll(bEnd, -height+1)
230 // horizontal relocation (scrolling)
231 if !b.Settings["softwrap"].(bool) {
232 cx := activeC.GetVisualX()
233 rw := runewidth.RuneWidth(activeC.RuneUnder(activeC.X))
235 rw = 1 // tab or newline
242 if cx+w.gutterOffset+rw > w.StartCol+w.Width {
243 w.StartCol = cx - w.Width + w.gutterOffset + rw
250 // LocFromVisual takes a visual location (x and y position) and returns the
251 // position in the buffer corresponding to the visual location
252 // If the requested position does not correspond to a buffer location it returns
253 // the nearest position
254 func (w *BufWindow) LocFromVisual(svloc buffer.Loc) buffer.Loc {
255 vx := svloc.X - w.X - w.gutterOffset
260 SLoc: w.Scroll(w.StartLine, svloc.Y-w.Y),
261 VisualX: vx + w.StartCol,
263 return w.LocFromVLoc(vloc)
266 func (w *BufWindow) drawGutter(vloc *buffer.Loc, bloc *buffer.Loc) {
269 for _, m := range w.Buf.Messages {
270 if m.Start.Y == bloc.Y || m.End.Y == bloc.Y {
276 screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, char, nil, s)
278 screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, char, nil, s)
282 func (w *BufWindow) drawDiffGutter(backgroundStyle tcell.Style, softwrapped bool, vloc *buffer.Loc, bloc *buffer.Loc) {
286 switch w.Buf.DiffStatus(bloc.Y) {
288 symbol = '\u258C' // Left half block
289 styleName = "diff-added"
290 case buffer.DSModified:
291 symbol = '\u258C' // Left half block
292 styleName = "diff-modified"
293 case buffer.DSDeletedAbove:
295 symbol = '\u2594' // Upper one eighth block
296 styleName = "diff-deleted"
300 style := backgroundStyle
301 if s, ok := config.Colorscheme[styleName]; ok {
302 foreground, _, _ := s.Decompose()
303 style = style.Foreground(foreground)
306 screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, symbol, nil, style)
310 func (w *BufWindow) drawLineNum(lineNumStyle tcell.Style, softwrapped bool, vloc *buffer.Loc, bloc *buffer.Loc) {
311 cursorLine := w.Buf.GetActiveCursor().Loc.Y
313 if w.Buf.Settings["relativeruler"] == false || cursorLine == bloc.Y {
316 lineInt = bloc.Y - cursorLine
318 lineNum := strconv.Itoa(util.Abs(lineInt))
320 // Write the spaces before the line number if necessary
321 for i := 0; i < w.maxLineNumLength-len(lineNum); i++ {
322 screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, ' ', nil, lineNumStyle)
325 // Write the actual line number
326 for _, ch := range lineNum {
328 screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, ' ', nil, lineNumStyle)
330 screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, ch, nil, lineNumStyle)
335 // Write the extra space
336 screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, ' ', nil, lineNumStyle)
340 // getStyle returns the highlight style for the given character position
341 // If there is no change to the current highlight style it just returns that
342 func (w *BufWindow) getStyle(style tcell.Style, bloc buffer.Loc) (tcell.Style, bool) {
343 if group, ok := w.Buf.Match(bloc.Y)[bloc.X]; ok {
344 s := config.GetColor(group.String())
350 func (w *BufWindow) showCursor(x, y int, main bool) {
353 screen.ShowCursor(x, y)
355 screen.ShowFakeCursorMulti(x, y)
360 // displayBuffer draws the buffer being shown in this window on the screen.Screen
361 func (w *BufWindow) displayBuffer() {
364 if w.Height <= 0 || w.Width <= 0 {
368 maxWidth := w.gutterOffset + w.bufWidth
370 if b.ModifiedThisFrame {
371 if b.Settings["diffgutter"].(bool) {
372 b.UpdateDiff(func(synchronous bool) {
373 // If the diff was updated asynchronously, the outer call to
374 // displayBuffer might already be completed and we need to
375 // schedule a redraw in order to display the new diff.
376 // Note that this cannot lead to an infinite recursion
377 // because the modifications were cleared above so there won't
378 // be another call to UpdateDiff when displayBuffer is called
379 // during the redraw.
385 b.ModifiedThisFrame = false
388 var matchingBraces []buffer.Loc
389 // bracePairs is defined in buffer.go
390 if b.Settings["matchbrace"].(bool) {
391 for _, bp := range buffer.BracePairs {
392 for _, c := range b.GetCursors() {
393 if c.HasSelection() {
399 r := c.RuneUnder(curX)
400 rl := c.RuneUnder(curX - 1)
401 if r == bp[0] || r == bp[1] || rl == bp[0] || rl == bp[1] {
402 mb, left, found := b.FindMatchingBrace(bp, curLoc)
404 matchingBraces = append(matchingBraces, mb)
406 matchingBraces = append(matchingBraces, curLoc)
408 matchingBraces = append(matchingBraces, curLoc.Move(-1, b))
416 lineNumStyle := config.DefStyle
417 if style, ok := config.Colorscheme["line-number"]; ok {
420 curNumStyle := config.DefStyle
421 if style, ok := config.Colorscheme["current-line-number"]; ok {
422 if !b.Settings["cursorline"].(bool) {
423 curNumStyle = lineNumStyle
429 softwrap := b.Settings["softwrap"].(bool)
430 wordwrap := softwrap && b.Settings["wordwrap"].(bool)
432 tabsize := util.IntOpt(b.Settings["tabsize"])
433 colorcolumn := util.IntOpt(b.Settings["colorcolumn"])
435 // this represents the current draw position
436 // within the current window
437 vloc := buffer.Loc{X: 0, Y: 0}
439 // the start line may be partially out of the current window
440 vloc.Y = -w.StartLine.Row
443 // this represents the current draw position in the buffer (char positions)
444 bloc := buffer.Loc{X: -1, Y: w.StartLine.Line}
446 cursors := b.GetCursors()
448 curStyle := config.DefStyle
449 for ; vloc.Y < w.bufHeight; vloc.Y++ {
453 for _, c := range cursors {
454 if bloc.Y == c.Y && w.active {
467 w.drawGutter(&vloc, &bloc)
470 if b.Settings["diffgutter"].(bool) {
471 w.drawDiffGutter(s, false, &vloc, &bloc)
474 if b.Settings["ruler"].(bool) {
475 w.drawLineNum(s, false, &vloc, &bloc)
478 vloc.X = w.gutterOffset
481 line, nColsBeforeStart, bslice, startStyle := w.getStartInfo(w.StartCol, bloc.Y)
482 if startStyle != nil {
483 curStyle = *startStyle
487 draw := func(r rune, combc []rune, style tcell.Style, highlight bool, showcursor bool) {
488 if nColsBeforeStart <= 0 && vloc.Y >= 0 {
490 if w.Buf.HighlightSearch && w.Buf.SearchMatch(bloc) {
491 style = config.DefStyle.Reverse(true)
492 if s, ok := config.Colorscheme["hlsearch"]; ok {
497 _, origBg, _ := style.Decompose()
498 _, defBg, _ := config.DefStyle.Decompose()
500 // syntax or hlsearch highlighting with non-default background takes precedence
501 // over cursor-line and color-column
502 dontOverrideBackground := origBg != defBg
504 for _, c := range cursors {
505 if c.HasSelection() &&
506 (bloc.GreaterEqual(c.CurSelection[0]) && bloc.LessThan(c.CurSelection[1]) ||
507 bloc.LessThan(c.CurSelection[0]) && bloc.GreaterEqual(c.CurSelection[1])) {
508 // The current character is selected
509 style = config.DefStyle.Reverse(true)
511 if s, ok := config.Colorscheme["selection"]; ok {
516 if b.Settings["cursorline"].(bool) && w.active && !dontOverrideBackground &&
517 !c.HasSelection() && c.Y == bloc.Y {
518 if s, ok := config.Colorscheme["cursor-line"]; ok {
519 fg, _, _ := s.Decompose()
520 style = style.Background(fg)
525 for _, m := range b.Messages {
526 if bloc.GreaterEqual(m.Start) && bloc.LessThan(m.End) ||
527 bloc.LessThan(m.End) && bloc.GreaterEqual(m.Start) {
528 style = style.Underline(true)
534 indentrunes := []rune(b.Settings["indentchar"].(string))
535 // if empty indentchar settings, use space
536 if len(indentrunes) == 0 {
537 indentrunes = []rune{' '}
541 if s, ok := config.Colorscheme["indent-char"]; ok && r != ' ' {
542 fg, _, _ := s.Decompose()
543 style = style.Foreground(fg)
547 if s, ok := config.Colorscheme["color-column"]; ok {
548 if colorcolumn != 0 && vloc.X-w.gutterOffset+w.StartCol == colorcolumn && !dontOverrideBackground {
549 fg, _, _ := s.Decompose()
550 style = style.Background(fg)
554 for _, mb := range matchingBraces {
555 if mb.X == bloc.X && mb.Y == bloc.Y {
556 style = style.Underline(true)
561 screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, r, combc, style)
564 for _, c := range cursors {
565 if c.X == bloc.X && c.Y == bloc.Y && !c.HasSelection() {
566 w.showCursor(w.X+vloc.X, w.Y+vloc.Y, c.Num == 0)
571 if nColsBeforeStart <= 0 {
580 w.drawGutter(&vloc, &bloc)
582 if b.Settings["diffgutter"].(bool) {
583 w.drawDiffGutter(lineNumStyle, true, &vloc, &bloc)
586 // This will draw an empty line number because the current line is wrapped
587 if b.Settings["ruler"].(bool) {
588 w.drawLineNum(lineNumStyle, true, &vloc, &bloc)
601 word = make([]glyph, 0, w.bufWidth)
603 word = make([]glyph, 0, 1)
607 totalwidth := w.StartCol - nColsBeforeStart
609 r, combc, size := util.DecodeCharacter(line)
612 loc := buffer.Loc{X: bloc.X + len(word), Y: bloc.Y}
613 curStyle, _ = w.getStyle(curStyle, loc)
619 ts := tabsize - (totalwidth % tabsize)
620 width = util.Min(ts, maxWidth-vloc.X)
623 width = runewidth.RuneWidth(r)
627 word = append(word, glyph{r, combc, curStyle, width})
630 // Collect a complete word to know its width.
631 // If wordwrap is off, every single character is a complete "word".
633 if !util.IsWhitespace(r) && len(line) > 0 && wordwidth < w.bufWidth {
638 // If a word (or just a wide rune) does not fit in the window
639 if vloc.X+wordwidth > maxWidth && vloc.X > w.gutterOffset {
640 for vloc.X < maxWidth {
641 draw(' ', nil, config.DefStyle, false, false)
644 // We either stop or we wrap to draw the word in the next line
649 if vloc.Y >= w.bufHeight {
656 for _, r := range word {
657 draw(r.r, r.combc, r.style, true, true)
659 // Draw any extra characters either spaces for tabs or @ for incomplete wide runes
666 for i := 1; i < r.width; i++ {
667 draw(char, nil, r.style, true, false)
676 // If we reach the end of the window then we either stop or we wrap for softwrap
677 if vloc.X >= maxWidth {
682 if vloc.Y >= w.bufHeight {
690 style := config.DefStyle
691 for _, c := range cursors {
692 if b.Settings["cursorline"].(bool) && w.active &&
693 !c.HasSelection() && c.Y == bloc.Y {
694 if s, ok := config.Colorscheme["cursor-line"]; ok {
695 fg, _, _ := s.Decompose()
696 style = style.Background(fg)
700 for i := vloc.X; i < maxWidth; i++ {
702 if s, ok := config.Colorscheme["color-column"]; ok {
703 if colorcolumn != 0 && i-w.gutterOffset+w.StartCol == colorcolumn {
704 fg, _, _ := s.Decompose()
705 curStyle = style.Background(fg)
708 screen.SetContent(i+w.X, vloc.Y+w.Y, ' ', nil, curStyle)
711 if vloc.X != maxWidth {
712 // Display newline within a selection
713 draw(' ', nil, config.DefStyle, true, true)
718 if bloc.Y >= b.LinesNum() {
724 func (w *BufWindow) displayStatusLine() {
725 if w.Buf.Settings["statusline"].(bool) {
727 } else if w.drawDivider {
728 divchars := config.GetGlobalOption("divchars").(string)
729 if util.CharacterCountInString(divchars) != 2 {
733 _, _, size := util.DecodeCharacterInString(divchars)
734 divchar, combc, _ := util.DecodeCharacterInString(divchars[size:])
736 dividerStyle := config.DefStyle
737 if style, ok := config.Colorscheme["divider"]; ok {
741 divreverse := config.GetGlobalOption("divreverse").(bool)
743 dividerStyle = dividerStyle.Reverse(true)
746 for x := w.X; x < w.X+w.Width; x++ {
747 screen.SetContent(x, w.Y+w.Height-1, divchar, combc, dividerStyle)
752 func (w *BufWindow) displayScrollBar() {
753 if w.Buf.Settings["scrollbar"].(bool) && w.Buf.LinesNum() > w.Height {
754 scrollX := w.X + w.Width - 1
755 barsize := int(float64(w.Height) / float64(w.Buf.LinesNum()) * float64(w.Height))
759 barstart := w.Y + int(float64(w.StartLine.Line)/float64(w.Buf.LinesNum())*float64(w.Height))
761 scrollBarStyle := config.DefStyle.Reverse(true)
762 if style, ok := config.Colorscheme["scrollbar"]; ok {
763 scrollBarStyle = style
766 for y := barstart; y < util.Min(barstart+barsize, w.Y+w.bufHeight); y++ {
767 screen.SetContent(scrollX, y, '|', nil, scrollBarStyle)
772 // Display displays the buffer and the statusline
773 func (w *BufWindow) Display() {
774 w.updateDisplayInfo()
776 w.displayStatusLine()