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