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