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