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