]> git.lizzy.rs Git - micro.git/blob - internal/display/bufwindow.go
20bac4abd0ffded47fd58ba92d7884eb6f80f446
[micro.git] / internal / display / bufwindow.go
1 package display
2
3 import (
4         "strconv"
5         "unicode/utf8"
6
7         runewidth "github.com/mattn/go-runewidth"
8         "github.com/zyedidia/micro/internal/buffer"
9         "github.com/zyedidia/micro/internal/config"
10         "github.com/zyedidia/micro/internal/screen"
11         "github.com/zyedidia/micro/internal/util"
12         "github.com/zyedidia/tcell"
13 )
14
15 // The BufWindow provides a way of displaying a certain section
16 // of a buffer
17 type BufWindow struct {
18         *View
19
20         // Buffer being shown in this window
21         Buf *buffer.Buffer
22
23         active bool
24
25         sline *StatusLine
26
27         gutterOffset int
28         drawStatus   bool
29 }
30
31 // NewBufWindow creates a new window at a location in the screen with a width and height
32 func NewBufWindow(x, y, width, height int, buf *buffer.Buffer) *BufWindow {
33         w := new(BufWindow)
34         w.View = new(View)
35         w.X, w.Y, w.Width, w.Height, w.Buf = x, y, width, height, buf
36         w.active = true
37
38         w.sline = NewStatusLine(w)
39
40         return w
41 }
42
43 func (w *BufWindow) SetBuffer(b *buffer.Buffer) {
44         w.Buf = b
45 }
46
47 func (w *BufWindow) GetView() *View {
48         return w.View
49 }
50
51 func (w *BufWindow) SetView(view *View) {
52         w.View = view
53 }
54
55 func (w *BufWindow) Resize(width, height int) {
56         w.Width, w.Height = width, height
57         w.Relocate()
58 }
59
60 func (w *BufWindow) SetActive(b bool) {
61         w.active = b
62 }
63
64 func (w *BufWindow) IsActive() bool {
65         return w.active
66 }
67
68 func (w *BufWindow) getStartInfo(n, lineN int) ([]byte, int, int, *tcell.Style) {
69         tabsize := util.IntOpt(w.Buf.Settings["tabsize"])
70         width := 0
71         bloc := buffer.Loc{0, lineN}
72         b := w.Buf.LineBytes(lineN)
73         curStyle := config.DefStyle
74         var s *tcell.Style
75         for len(b) > 0 {
76                 r, size := utf8.DecodeRune(b)
77
78                 curStyle, found := w.getStyle(curStyle, bloc, r)
79                 if found {
80                         s = &curStyle
81                 }
82
83                 w := 0
84                 switch r {
85                 case '\t':
86                         ts := tabsize - (width % tabsize)
87                         w = ts
88                 default:
89                         w = runewidth.RuneWidth(r)
90                 }
91                 if width+w > n {
92                         return b, n - width, bloc.X, s
93                 }
94                 width += w
95                 b = b[size:]
96                 bloc.X++
97         }
98         return b, n - width, bloc.X, s
99 }
100
101 // Clear resets all cells in this window to the default style
102 func (w *BufWindow) Clear() {
103         for y := 0; y < w.Height; y++ {
104                 for x := 0; x < w.Width; x++ {
105                         screen.SetContent(w.X+x, w.Y+y, ' ', nil, config.DefStyle)
106                 }
107         }
108 }
109
110 // Bottomline returns the line number of the lowest line in the view
111 // You might think that this is obviously just v.StartLine + v.Height
112 // but if softwrap is enabled things get complicated since one buffer
113 // line can take up multiple lines in the view
114 func (w *BufWindow) Bottomline() int {
115         if !w.Buf.Settings["softwrap"].(bool) {
116                 h := w.StartLine + w.Height - 1
117                 if w.drawStatus {
118                         h--
119                 }
120                 return h
121         }
122
123         l := w.LocFromVisual(buffer.Loc{0, w.Y + w.Height})
124
125         return l.Y
126 }
127
128 // Relocate moves the view window so that the cursor is in view
129 // This is useful if the user has scrolled far away, and then starts typing
130 // Returns true if the window location is moved
131 func (w *BufWindow) Relocate() bool {
132         b := w.Buf
133         // how many buffer lines are in the view
134         height := w.Bottomline() + 1 - w.StartLine
135         h := w.Height
136         if w.drawStatus {
137                 h--
138         }
139         if b.LinesNum() <= h {
140                 height = w.Height
141         }
142         ret := false
143         activeC := w.Buf.GetActiveCursor()
144         cy := activeC.Y
145         scrollmargin := int(b.Settings["scrollmargin"].(float64))
146         if cy < w.StartLine+scrollmargin && cy > scrollmargin-1 {
147                 w.StartLine = cy - scrollmargin
148                 ret = true
149         } else if cy < w.StartLine {
150                 w.StartLine = cy
151                 ret = true
152         }
153         if cy > w.StartLine+height-1-scrollmargin && cy < b.LinesNum()-scrollmargin {
154                 w.StartLine = cy - height + 1 + scrollmargin
155                 ret = true
156         } else if cy >= b.LinesNum()-scrollmargin && cy >= height {
157                 w.StartLine = b.LinesNum() - height
158                 ret = true
159         }
160
161         // horizontal relocation (scrolling)
162         if !b.Settings["softwrap"].(bool) {
163                 cx := activeC.GetVisualX()
164                 if cx < w.StartCol {
165                         w.StartCol = cx
166                         ret = true
167                 }
168                 if cx+w.gutterOffset+1 > w.StartCol+w.Width {
169                         w.StartCol = cx - w.Width + w.gutterOffset + 1
170                         ret = true
171                 }
172         }
173         return ret
174 }
175
176 // LocFromVisual takes a visual location (x and y position) and returns the
177 // position in the buffer corresponding to the visual location
178 // Computing the buffer location requires essentially drawing the entire screen
179 // to account for complications like softwrap, wide characters, and horizontal scrolling
180 // If the requested position does not correspond to a buffer location it returns
181 // the nearest position
182 func (w *BufWindow) LocFromVisual(svloc buffer.Loc) buffer.Loc {
183         b := w.Buf
184
185         hasMessage := len(b.Messages) > 0
186         bufHeight := w.Height
187         if w.drawStatus {
188                 bufHeight--
189         }
190
191         bufWidth := w.Width
192         if w.Buf.Settings["scrollbar"].(bool) && w.Buf.LinesNum() > w.Height {
193                 bufWidth--
194         }
195
196         // We need to know the string length of the largest line number
197         // so we can pad appropriately when displaying line numbers
198         maxLineNumLength := len(strconv.Itoa(b.LinesNum()))
199
200         tabsize := int(b.Settings["tabsize"].(float64))
201         softwrap := b.Settings["softwrap"].(bool)
202
203         // this represents the current draw position
204         // within the current window
205         vloc := buffer.Loc{X: 0, Y: 0}
206
207         // this represents the current draw position in the buffer (char positions)
208         bloc := buffer.Loc{X: -1, Y: w.StartLine}
209
210         for vloc.Y = 0; vloc.Y < bufHeight; vloc.Y++ {
211                 vloc.X = 0
212                 if hasMessage {
213                         vloc.X += 2
214                 }
215                 if b.Settings["ruler"].(bool) {
216                         vloc.X += maxLineNumLength + 1
217                 }
218
219                 line := b.LineBytes(bloc.Y)
220                 line, nColsBeforeStart, bslice := util.SliceVisualEnd(line, w.StartCol, tabsize)
221                 bloc.X = bslice
222
223                 draw := func() {
224                         if nColsBeforeStart <= 0 {
225                                 vloc.X++
226                         }
227                         nColsBeforeStart--
228                 }
229
230                 totalwidth := w.StartCol - nColsBeforeStart
231
232                 if svloc.X <= vloc.X+w.X && vloc.Y+w.Y == svloc.Y {
233                         return bloc
234                 }
235                 for len(line) > 0 {
236                         if vloc.X+w.X == svloc.X && vloc.Y+w.Y == svloc.Y {
237                                 return bloc
238                         }
239
240                         r, size := utf8.DecodeRune(line)
241                         draw()
242                         width := 0
243
244                         switch r {
245                         case '\t':
246                                 ts := tabsize - (totalwidth % tabsize)
247                                 width = ts
248                         default:
249                                 width = runewidth.RuneWidth(r)
250                         }
251
252                         // Draw any extra characters either spaces for tabs or @ for incomplete wide runes
253                         if width > 1 {
254                                 for i := 1; i < width; i++ {
255                                         if vloc.X+w.X == svloc.X && vloc.Y+w.Y == svloc.Y {
256                                                 return bloc
257                                         }
258                                         draw()
259                                 }
260                         }
261                         bloc.X++
262                         line = line[size:]
263
264                         totalwidth += width
265
266                         // If we reach the end of the window then we either stop or we wrap for softwrap
267                         if vloc.X >= bufWidth {
268                                 if !softwrap {
269                                         break
270                                 } else {
271                                         vloc.Y++
272                                         if vloc.Y >= bufHeight {
273                                                 break
274                                         }
275                                         vloc.X = 0
276                                         // This will draw an empty line number because the current line is wrapped
277                                         if b.Settings["ruler"].(bool) {
278                                                 vloc.X += maxLineNumLength + 1
279                                         }
280                                 }
281                         }
282                 }
283                 if vloc.Y+w.Y == svloc.Y {
284                         return bloc
285                 }
286
287                 if bloc.Y+1 >= b.LinesNum() || vloc.Y+1 >= bufHeight {
288                         return bloc
289                 }
290
291                 bloc.X = w.StartCol
292                 bloc.Y++
293         }
294
295         return buffer.Loc{}
296 }
297
298 func (w *BufWindow) drawGutter(vloc *buffer.Loc, bloc *buffer.Loc) {
299         char := ' '
300         s := config.DefStyle
301         for _, m := range w.Buf.Messages {
302                 if m.Start.Y == bloc.Y || m.End.Y == bloc.Y {
303                         s = m.Style()
304                         char = '>'
305                         break
306                 }
307         }
308         screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, char, nil, s)
309         vloc.X++
310         screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, char, nil, s)
311         vloc.X++
312 }
313
314 func (w *BufWindow) drawLineNum(lineNumStyle tcell.Style, softwrapped bool, maxLineNumLength int, vloc *buffer.Loc, bloc *buffer.Loc) {
315         lineNum := strconv.Itoa(bloc.Y + 1)
316
317         // Write the spaces before the line number if necessary
318         for i := 0; i < maxLineNumLength-len(lineNum); i++ {
319                 screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, ' ', nil, lineNumStyle)
320                 vloc.X++
321         }
322         // Write the actual line number
323         for _, ch := range lineNum {
324                 if softwrapped {
325                         screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, ' ', nil, lineNumStyle)
326                 } else {
327                         screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, ch, nil, lineNumStyle)
328                 }
329                 vloc.X++
330         }
331
332         // Write the extra space
333         screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, ' ', nil, lineNumStyle)
334         vloc.X++
335 }
336
337 // getStyle returns the highlight style for the given character position
338 // If there is no change to the current highlight style it just returns that
339 func (w *BufWindow) getStyle(style tcell.Style, bloc buffer.Loc, r rune) (tcell.Style, bool) {
340         if group, ok := w.Buf.Match(bloc.Y)[bloc.X]; ok {
341                 s := config.GetColor(group.String())
342                 return s, true
343         }
344         return style, false
345 }
346
347 func (w *BufWindow) showCursor(x, y int, main bool) {
348         if w.active {
349                 if main {
350                         screen.ShowCursor(x, y)
351                 } else {
352                         screen.ShowFakeCursorMulti(x, y)
353                 }
354         }
355 }
356
357 // displayBuffer draws the buffer being shown in this window on the screen.Screen
358 func (w *BufWindow) displayBuffer() {
359         b := w.Buf
360
361         if w.Height <= 0 || w.Width <= 0 {
362                 return
363         }
364
365         hasMessage := len(b.Messages) > 0
366         bufHeight := w.Height
367         if w.drawStatus {
368                 bufHeight--
369         }
370
371         bufWidth := w.Width
372         if w.Buf.Settings["scrollbar"].(bool) && w.Buf.LinesNum() > w.Height {
373                 bufWidth--
374         }
375
376         if b.Settings["syntax"].(bool) && b.SyntaxDef != nil {
377                 for _, r := range b.Modifications {
378                         final := -1
379                         for i := r.X; i <= r.Y; i++ {
380                                 final = util.Max(b.Highlighter.ReHighlightStates(b, i), final)
381                         }
382                         b.Highlighter.HighlightMatches(b, r.X, final+1)
383                 }
384                 b.ClearModifications()
385         }
386
387         var matchingBraces []buffer.Loc
388         // bracePairs is defined in buffer.go
389         if b.Settings["matchbrace"].(bool) {
390                 for _, bp := range buffer.BracePairs {
391                         for _, c := range b.GetCursors() {
392                                 if c.HasSelection() {
393                                         continue
394                                 }
395                                 curX := c.X
396                                 curLoc := c.Loc
397
398                                 r := c.RuneUnder(curX)
399                                 rl := c.RuneUnder(curX - 1)
400                                 if r == bp[0] || r == bp[1] || rl == bp[0] || rl == bp[1] {
401                                         mb, left := b.FindMatchingBrace(bp, curLoc)
402                                         matchingBraces = append(matchingBraces, mb)
403                                         if !left {
404                                                 matchingBraces = append(matchingBraces, curLoc)
405                                         } else {
406                                                 matchingBraces = append(matchingBraces, curLoc.Move(-1, b))
407                                         }
408                                 }
409                         }
410                 }
411         }
412
413         lineNumStyle := config.DefStyle
414         if style, ok := config.Colorscheme["line-number"]; ok {
415                 lineNumStyle = style
416         }
417         curNumStyle := config.DefStyle
418         if style, ok := config.Colorscheme["current-line-number"]; ok {
419                 curNumStyle = style
420                 if !b.Settings["cursorline"].(bool) {
421                         _, bg, _ := lineNumStyle.Decompose()
422                         curNumStyle = curNumStyle.Background(bg)
423                 }
424         }
425
426         // We need to know the string length of the largest line number
427         // so we can pad appropriately when displaying line numbers
428         maxLineNumLength := len(strconv.Itoa(b.LinesNum()))
429
430         softwrap := b.Settings["softwrap"].(bool)
431         tabsize := util.IntOpt(b.Settings["tabsize"])
432         colorcolumn := util.IntOpt(b.Settings["colorcolumn"])
433
434         // this represents the current draw position
435         // within the current window
436         vloc := buffer.Loc{X: 0, Y: 0}
437
438         // this represents the current draw position in the buffer (char positions)
439         bloc := buffer.Loc{X: -1, Y: w.StartLine}
440
441         cursors := b.GetCursors()
442
443         curStyle := config.DefStyle
444         for vloc.Y = 0; vloc.Y < bufHeight; vloc.Y++ {
445                 vloc.X = 0
446
447                 if hasMessage {
448                         w.drawGutter(&vloc, &bloc)
449                 }
450
451                 if b.Settings["ruler"].(bool) {
452                         s := lineNumStyle
453                         for _, c := range cursors {
454                                 if bloc.Y == c.Y && w.active {
455                                         s = curNumStyle
456                                         break
457                                 }
458                         }
459                         w.drawLineNum(s, false, maxLineNumLength, &vloc, &bloc)
460                 }
461
462                 w.gutterOffset = vloc.X
463
464                 line, nColsBeforeStart, bslice, startStyle := w.getStartInfo(w.StartCol, bloc.Y)
465                 if startStyle != nil {
466                         curStyle = *startStyle
467                 }
468                 bloc.X = bslice
469
470                 draw := func(r rune, style tcell.Style, showcursor bool) {
471                         if nColsBeforeStart <= 0 {
472                                 for _, c := range cursors {
473                                         if c.HasSelection() &&
474                                                 (bloc.GreaterEqual(c.CurSelection[0]) && bloc.LessThan(c.CurSelection[1]) ||
475                                                         bloc.LessThan(c.CurSelection[0]) && bloc.GreaterEqual(c.CurSelection[1])) {
476                                                 // The current character is selected
477                                                 style = config.DefStyle.Reverse(true)
478
479                                                 if s, ok := config.Colorscheme["selection"]; ok {
480                                                         style = s
481                                                 }
482                                         }
483
484                                         if b.Settings["cursorline"].(bool) && w.active &&
485                                                 !c.HasSelection() && c.Y == bloc.Y {
486                                                 if s, ok := config.Colorscheme["cursor-line"]; ok {
487                                                         fg, _, _ := s.Decompose()
488                                                         style = style.Background(fg)
489                                                 }
490                                         }
491                                 }
492
493                                 for _, m := range b.Messages {
494                                         if bloc.GreaterEqual(m.Start) && bloc.LessThan(m.End) ||
495                                                 bloc.LessThan(m.End) && bloc.GreaterEqual(m.Start) {
496                                                 style = style.Underline(true)
497                                                 break
498                                         }
499                                 }
500
501                                 if r == '\t' {
502                                         indentrunes := []rune(b.Settings["indentchar"].(string))
503                                         // if empty indentchar settings, use space
504                                         if indentrunes == nil || len(indentrunes) == 0 {
505                                                 indentrunes = []rune{' '}
506                                         }
507
508                                         r = indentrunes[0]
509                                         if s, ok := config.Colorscheme["indent-char"]; ok && r != ' ' {
510                                                 fg, _, _ := s.Decompose()
511                                                 style = style.Foreground(fg)
512                                         }
513                                 }
514
515                                 if s, ok := config.Colorscheme["color-column"]; ok {
516                                         if colorcolumn != 0 && vloc.X-w.gutterOffset == colorcolumn {
517                                                 fg, _, _ := s.Decompose()
518                                                 style = style.Background(fg)
519                                         }
520                                 }
521
522                                 for _, mb := range matchingBraces {
523                                         if mb.X == bloc.X && mb.Y == bloc.Y {
524                                                 style = style.Underline(true)
525                                         }
526                                 }
527
528                                 screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, r, nil, style)
529
530                                 if showcursor {
531                                         for _, c := range cursors {
532                                                 if c.X == bloc.X && c.Y == bloc.Y && !c.HasSelection() {
533                                                         w.showCursor(w.X+vloc.X, w.Y+vloc.Y, c.Num == 0)
534                                                 }
535                                         }
536                                 }
537                                 vloc.X++
538                         }
539                         nColsBeforeStart--
540                 }
541
542                 totalwidth := w.StartCol - nColsBeforeStart
543                 for len(line) > 0 {
544                         r, size := utf8.DecodeRune(line)
545                         curStyle, _ = w.getStyle(curStyle, bloc, r)
546
547                         draw(r, curStyle, true)
548
549                         width := 0
550
551                         char := ' '
552                         switch r {
553                         case '\t':
554                                 ts := tabsize - (totalwidth % tabsize)
555                                 width = ts
556                         default:
557                                 width = runewidth.RuneWidth(r)
558                                 char = '@'
559                         }
560
561                         // Draw any extra characters either spaces for tabs or @ for incomplete wide runes
562                         if width > 1 {
563                                 for i := 1; i < width; i++ {
564                                         draw(char, curStyle, false)
565                                 }
566                         }
567                         bloc.X++
568                         line = line[size:]
569
570                         totalwidth += width
571
572                         // If we reach the end of the window then we either stop or we wrap for softwrap
573                         if vloc.X >= bufWidth {
574                                 if !softwrap {
575                                         break
576                                 } else {
577                                         vloc.Y++
578                                         if vloc.Y >= bufHeight {
579                                                 break
580                                         }
581                                         vloc.X = 0
582                                         // This will draw an empty line number because the current line is wrapped
583                                         if b.Settings["ruler"].(bool) {
584                                                 w.drawLineNum(lineNumStyle, true, maxLineNumLength, &vloc, &bloc)
585                                         }
586                                 }
587                         }
588                 }
589
590                 style := config.DefStyle
591                 for _, c := range cursors {
592                         if b.Settings["cursorline"].(bool) && w.active &&
593                                 !c.HasSelection() && c.Y == bloc.Y {
594                                 if s, ok := config.Colorscheme["cursor-line"]; ok {
595                                         fg, _, _ := s.Decompose()
596                                         style = style.Background(fg)
597                                 }
598                         }
599                 }
600                 for i := vloc.X; i < bufWidth; i++ {
601                         curStyle := style
602                         if s, ok := config.Colorscheme["color-column"]; ok {
603                                 if colorcolumn != 0 && i-w.gutterOffset == colorcolumn {
604                                         fg, _, _ := s.Decompose()
605                                         curStyle = style.Background(fg)
606                                 }
607                         }
608                         screen.SetContent(i+w.X, vloc.Y+w.Y, ' ', nil, curStyle)
609                 }
610
611                 if vloc.X != bufWidth {
612                         draw(' ', curStyle, true)
613                 }
614
615                 bloc.X = w.StartCol
616                 bloc.Y++
617                 if bloc.Y >= b.LinesNum() {
618                         break
619                 }
620         }
621 }
622
623 func (w *BufWindow) displayStatusLine() {
624         _, h := screen.Screen.Size()
625         infoY := h
626         if config.GetGlobalOption("infobar").(bool) {
627                 infoY--
628         }
629
630         if w.Buf.Settings["statusline"].(bool) {
631                 w.drawStatus = true
632                 w.sline.Display()
633         } else if w.Y+w.Height != infoY {
634                 w.drawStatus = true
635                 for x := w.X; x < w.X+w.Width; x++ {
636                         screen.SetContent(x, w.Y+w.Height-1, '-', nil, config.DefStyle.Reverse(true))
637                 }
638         } else {
639                 w.drawStatus = false
640         }
641 }
642
643 func (w *BufWindow) displayScrollBar() {
644         if w.Buf.Settings["scrollbar"].(bool) && w.Buf.LinesNum() > w.Height {
645                 scrollX := w.X + w.Width - 1
646                 bufHeight := w.Height
647                 if w.drawStatus {
648                         bufHeight--
649                 }
650                 barsize := int(float64(w.Height) / float64(w.Buf.LinesNum()) * float64(w.Height))
651                 if barsize < 1 {
652                         barsize = 1
653                 }
654                 barstart := w.Y + int(float64(w.StartLine)/float64(w.Buf.LinesNum())*float64(w.Height))
655                 for y := barstart; y < util.Min(barstart+barsize, w.Y+bufHeight); y++ {
656                         screen.SetContent(scrollX, y, '|', nil, config.DefStyle.Reverse(true))
657                 }
658         }
659 }
660
661 // Display displays the buffer and the statusline
662 func (w *BufWindow) Display() {
663         w.displayStatusLine()
664         w.displayScrollBar()
665         w.displayBuffer()
666 }