]> git.lizzy.rs Git - micro.git/blob - cmd/micro/view.go
b91805ee80d24846e587526adc01dc26a3f38773
[micro.git] / cmd / micro / view.go
1 package main
2
3 import (
4         "io/ioutil"
5         "strconv"
6         "strings"
7         "time"
8
9         "github.com/mattn/go-runewidth"
10         "github.com/mitchellh/go-homedir"
11         "github.com/zyedidia/tcell"
12 )
13
14 type ViewType int
15
16 const (
17         vtDefault ViewType = iota
18         vtHelp
19         vtLog
20 )
21
22 // The View struct stores information about a view into a buffer.
23 // It stores information about the cursor, and the viewport
24 // that the user sees the buffer from.
25 type View struct {
26         // A pointer to the buffer's cursor for ease of access
27         Cursor *Cursor
28
29         // The topmost line, used for vertical scrolling
30         Topline    int
31         Bottomline int
32         // The leftmost column, used for horizontal scrolling
33         leftCol int
34
35         // Specifies whether or not this view holds a help buffer
36         Type ViewType
37
38         // Actual width and height
39         width  int
40         height int
41
42         // Where this view is located
43         x, y int
44
45         // How much to offset because of line numbers
46         lineNumOffset int
47
48         // Holds the list of gutter messages
49         messages map[string][]GutterMessage
50
51         // This is the index of this view in the views array
52         Num int
53         // What tab is this view stored in
54         TabNum int
55
56         // The buffer
57         Buf *Buffer
58         // The statusline
59         sline Statusline
60
61         // Since tcell doesn't differentiate between a mouse release event
62         // and a mouse move event with no keys pressed, we need to keep
63         // track of whether or not the mouse was pressed (or not released) last event to determine
64         // mouse release events
65         mouseReleased bool
66
67         // This stores when the last click was
68         // This is useful for detecting double and triple clicks
69         lastClickTime time.Time
70
71         // lastCutTime stores when the last ctrl+k was issued.
72         // It is used for clearing the clipboard to replace it with fresh cut lines.
73         lastCutTime time.Time
74
75         // freshClip returns true if the clipboard has never been pasted.
76         freshClip bool
77
78         // Was the last mouse event actually a double click?
79         // Useful for detecting triple clicks -- if a double click is detected
80         // but the last mouse event was actually a double click, it's a triple click
81         doubleClick bool
82         // Same here, just to keep track for mouse move events
83         tripleClick bool
84
85         // Syntax highlighting matches
86         matches SyntaxMatches
87
88         splitNode *LeafNode
89 }
90
91 // NewView returns a new fullscreen view
92 func NewView(buf *Buffer) *View {
93         screenW, screenH := screen.Size()
94         return NewViewWidthHeight(buf, screenW, screenH)
95 }
96
97 // NewViewWidthHeight returns a new view with the specified width and height
98 // Note that w and h are raw column and row values
99 func NewViewWidthHeight(buf *Buffer, w, h int) *View {
100         v := new(View)
101
102         v.x, v.y = 0, 0
103
104         v.width = w
105         v.height = h
106
107         v.ToggleTabbar()
108
109         v.OpenBuffer(buf)
110
111         v.messages = make(map[string][]GutterMessage)
112
113         v.sline = Statusline{
114                 view: v,
115         }
116
117         if v.Buf.Settings["statusline"].(bool) {
118                 v.height--
119         }
120
121         for _, pl := range loadedPlugins {
122                 _, err := Call(pl+".onViewOpen", v)
123                 if err != nil && !strings.HasPrefix(err.Error(), "function does not exist") {
124                         TermMessage(err)
125                         continue
126                 }
127         }
128
129         return v
130 }
131
132 // ToggleStatusLine creates an extra row for the statusline if necessary
133 func (v *View) ToggleStatusLine() {
134         if v.Buf.Settings["statusline"].(bool) {
135                 v.height--
136         } else {
137                 v.height++
138         }
139 }
140
141 // ToggleTabbar creates an extra row for the tabbar if necessary
142 func (v *View) ToggleTabbar() {
143         if len(tabs) > 1 {
144                 if v.y == 0 {
145                         // Include one line for the tab bar at the top
146                         v.height--
147                         v.y = 1
148                 }
149         } else {
150                 if v.y == 1 {
151                         v.y = 0
152                         v.height++
153                 }
154         }
155 }
156
157 func (v *View) paste(clip string) {
158         leadingWS := GetLeadingWhitespace(v.Buf.Line(v.Cursor.Y))
159
160         if v.Cursor.HasSelection() {
161                 v.Cursor.DeleteSelection()
162                 v.Cursor.ResetSelection()
163         }
164         clip = strings.Replace(clip, "\n", "\n"+leadingWS, -1)
165         v.Buf.Insert(v.Cursor.Loc, clip)
166         v.Cursor.Loc = v.Cursor.Loc.Move(Count(clip), v.Buf)
167         v.freshClip = false
168         messenger.Message("Pasted clipboard")
169 }
170
171 // ScrollUp scrolls the view up n lines (if possible)
172 func (v *View) ScrollUp(n int) {
173         // Try to scroll by n but if it would overflow, scroll by 1
174         if v.Topline-n >= 0 {
175                 v.Topline -= n
176         } else if v.Topline > 0 {
177                 v.Topline--
178         }
179 }
180
181 // ScrollDown scrolls the view down n lines (if possible)
182 func (v *View) ScrollDown(n int) {
183         // Try to scroll by n but if it would overflow, scroll by 1
184         if v.Topline+n <= v.Buf.NumLines-v.height {
185                 v.Topline += n
186         } else if v.Topline < v.Buf.NumLines-v.height {
187                 v.Topline++
188         }
189 }
190
191 // CanClose returns whether or not the view can be closed
192 // If there are unsaved changes, the user will be asked if the view can be closed
193 // causing them to lose the unsaved changes
194 func (v *View) CanClose() bool {
195         if v.Type == vtDefault && v.Buf.IsModified {
196                 var char rune
197                 var canceled bool
198                 if v.Buf.Settings["autosave"].(bool) {
199                         char = 'y'
200                 } else {
201                         char, canceled = messenger.LetterPrompt("Save changes to "+v.Buf.Name+" before closing? (y,n,esc) ", 'y', 'n')
202                 }
203                 if !canceled {
204                         if char == 'y' {
205                                 v.Save(true)
206                                 return true
207                         } else if char == 'n' {
208                                 return true
209                         }
210                 }
211         } else {
212                 return true
213         }
214         return false
215 }
216
217 // OpenBuffer opens a new buffer in this view.
218 // This resets the topline, event handler and cursor.
219 func (v *View) OpenBuffer(buf *Buffer) {
220         screen.Clear()
221         v.CloseBuffer()
222         v.Buf = buf
223         v.Cursor = &buf.Cursor
224         v.Topline = 0
225         v.leftCol = 0
226         v.Cursor.ResetSelection()
227         v.Relocate()
228         v.Center(false)
229         v.messages = make(map[string][]GutterMessage)
230
231         v.matches = Match(v)
232
233         // Set mouseReleased to true because we assume the mouse is not being pressed when
234         // the editor is opened
235         v.mouseReleased = true
236         v.lastClickTime = time.Time{}
237 }
238
239 // Open opens the given file in the view
240 func (v *View) Open(filename string) {
241         home, _ := homedir.Dir()
242         filename = strings.Replace(filename, "~", home, 1)
243         file, err := ioutil.ReadFile(filename)
244
245         var buf *Buffer
246         if err != nil {
247                 messenger.Message(err.Error())
248                 // File does not exist -- create an empty buffer with that name
249                 buf = NewBuffer([]byte{}, filename)
250         } else {
251                 buf = NewBuffer(file, filename)
252         }
253         v.OpenBuffer(buf)
254 }
255
256 // CloseBuffer performs any closing functions on the buffer
257 func (v *View) CloseBuffer() {
258         if v.Buf != nil {
259                 v.Buf.Serialize()
260         }
261 }
262
263 // ReOpen reloads the current buffer
264 func (v *View) ReOpen() {
265         if v.CanClose() {
266                 screen.Clear()
267                 v.Buf.ReOpen()
268                 v.Relocate()
269                 v.matches = Match(v)
270         }
271 }
272
273 // HSplit opens a horizontal split with the given buffer
274 func (v *View) HSplit(buf *Buffer) bool {
275         v.splitNode.HSplit(buf)
276         tabs[v.TabNum].Resize()
277         return false
278 }
279
280 // VSplit opens a vertical split with the given buffer
281 func (v *View) VSplit(buf *Buffer) bool {
282         v.splitNode.VSplit(buf)
283         tabs[v.TabNum].Resize()
284         return false
285 }
286
287 // GetSoftWrapLocation gets the location of a visual click on the screen and converts it to col,line
288 func (v *View) GetSoftWrapLocation(vx, vy int) (int, int) {
289         if !v.Buf.Settings["softwrap"].(bool) {
290                 vx = v.Cursor.GetCharPosInLine(vy, vx)
291                 return vx, vy
292         }
293
294         screenX, screenY := 0, v.Topline
295         for lineN := v.Topline; lineN < v.Bottomline; lineN++ {
296                 line := v.Buf.Line(lineN)
297
298                 colN := 0
299                 for _, ch := range line {
300                         if screenX >= v.width-v.lineNumOffset {
301                                 screenX = 0
302                                 screenY++
303                         }
304
305                         if screenX == vx && screenY == vy {
306                                 return colN, lineN
307                         }
308
309                         if ch == '\t' {
310                                 screenX += int(v.Buf.Settings["tabsize"].(float64)) - 1
311                         }
312
313                         screenX++
314                         colN++
315                 }
316                 if screenY == vy {
317                         return colN, lineN
318                 }
319                 screenX = 0
320                 screenY++
321         }
322
323         return 0, 0
324 }
325
326 // Relocate moves the view window so that the cursor is in view
327 // This is useful if the user has scrolled far away, and then starts typing
328 func (v *View) Relocate() bool {
329         height := v.Bottomline - v.Topline
330         ret := false
331         cy := v.Cursor.Y
332         scrollmargin := int(v.Buf.Settings["scrollmargin"].(float64))
333         if cy < v.Topline+scrollmargin && cy > scrollmargin-1 {
334                 v.Topline = cy - scrollmargin
335                 ret = true
336         } else if cy < v.Topline {
337                 v.Topline = cy
338                 ret = true
339         }
340         if cy > v.Topline+height-1-scrollmargin && cy < v.Buf.NumLines-scrollmargin {
341                 v.Topline = cy - height + 1 + scrollmargin
342                 ret = true
343         } else if cy >= v.Buf.NumLines-scrollmargin && cy > height {
344                 v.Topline = v.Buf.NumLines - height
345                 ret = true
346         }
347
348         if !v.Buf.Settings["softwrap"].(bool) {
349                 cx := v.Cursor.GetVisualX()
350                 if cx < v.leftCol {
351                         v.leftCol = cx
352                         ret = true
353                 }
354                 if cx+v.lineNumOffset+1 > v.leftCol+v.width {
355                         v.leftCol = cx - v.width + v.lineNumOffset + 1
356                         ret = true
357                 }
358         }
359         return ret
360 }
361
362 // MoveToMouseClick moves the cursor to location x, y assuming x, y were given
363 // by a mouse click
364 func (v *View) MoveToMouseClick(x, y int) {
365         if y-v.Topline > v.height-1 {
366                 v.ScrollDown(1)
367                 y = v.height + v.Topline - 1
368         }
369         if y >= v.Buf.NumLines {
370                 y = v.Buf.NumLines - 1
371         }
372         if y < 0 {
373                 y = 0
374         }
375         if x < 0 {
376                 x = 0
377         }
378
379         x, y = v.GetSoftWrapLocation(x, y)
380         // x = v.Cursor.GetCharPosInLine(y, x)
381         if x > Count(v.Buf.Line(y)) {
382                 x = Count(v.Buf.Line(y))
383         }
384         v.Cursor.X = x
385         v.Cursor.Y = y
386         v.Cursor.LastVisualX = v.Cursor.GetVisualX()
387 }
388
389 // HandleEvent handles an event passed by the main loop
390 func (v *View) HandleEvent(event tcell.Event) {
391         // This bool determines whether the view is relocated at the end of the function
392         // By default it's true because most events should cause a relocate
393         relocate := true
394
395         v.Buf.CheckModTime()
396
397         switch e := event.(type) {
398         case *tcell.EventResize:
399                 // Window resized
400                 tabs[v.TabNum].Resize()
401         case *tcell.EventKey:
402                 // Check first if input is a key binding, if it is we 'eat' the input and don't insert a rune
403                 isBinding := false
404                 if e.Key() != tcell.KeyRune || e.Modifiers() != 0 {
405                         for key, actions := range bindings {
406                                 if e.Key() == key.keyCode {
407                                         if e.Key() == tcell.KeyRune {
408                                                 if e.Rune() != key.r {
409                                                         continue
410                                                 }
411                                         }
412                                         if e.Modifiers() == key.modifiers {
413                                                 relocate = false
414                                                 isBinding = true
415                                                 for _, action := range actions {
416                                                         relocate = action(v, true) || relocate
417                                                         funcName := FuncName(action)
418                                                         if funcName != "main.(*View).ToggleMacro" && funcName != "main.(*View).PlayMacro" {
419                                                                 if recordingMacro {
420                                                                         curMacro = append(curMacro, action)
421                                                                 }
422                                                         }
423                                                 }
424                                                 break
425                                         }
426                                 }
427                         }
428                 }
429                 if !isBinding && e.Key() == tcell.KeyRune {
430                         // Insert a character
431                         if v.Cursor.HasSelection() {
432                                 v.Cursor.DeleteSelection()
433                                 v.Cursor.ResetSelection()
434                         }
435                         v.Buf.Insert(v.Cursor.Loc, string(e.Rune()))
436                         v.Cursor.Right()
437
438                         for _, pl := range loadedPlugins {
439                                 _, err := Call(pl+".onRune", string(e.Rune()), v)
440                                 if err != nil && !strings.HasPrefix(err.Error(), "function does not exist") {
441                                         TermMessage(err)
442                                 }
443                         }
444
445                         if recordingMacro {
446                                 curMacro = append(curMacro, e.Rune())
447                         }
448                 }
449         case *tcell.EventPaste:
450                 if !PreActionCall("Paste", v) {
451                         break
452                 }
453
454                 leadingWS := GetLeadingWhitespace(v.Buf.Line(v.Cursor.Y))
455
456                 if v.Cursor.HasSelection() {
457                         v.Cursor.DeleteSelection()
458                         v.Cursor.ResetSelection()
459                 }
460                 clip := e.Text()
461                 clip = strings.Replace(clip, "\n", "\n"+leadingWS, -1)
462                 v.Buf.Insert(v.Cursor.Loc, clip)
463                 v.Cursor.Loc = v.Cursor.Loc.Move(Count(clip), v.Buf)
464                 v.freshClip = false
465                 messenger.Message("Pasted clipboard")
466
467                 PostActionCall("Paste", v)
468         case *tcell.EventMouse:
469                 x, y := e.Position()
470                 x -= v.lineNumOffset - v.leftCol + v.x
471                 y += v.Topline - v.y
472                 // Don't relocate for mouse events
473                 relocate = false
474
475                 button := e.Buttons()
476
477                 switch button {
478                 case tcell.Button1:
479                         // Left click
480                         if v.mouseReleased {
481                                 v.MoveToMouseClick(x, y)
482                                 if time.Since(v.lastClickTime)/time.Millisecond < doubleClickThreshold {
483                                         if v.doubleClick {
484                                                 // Triple click
485                                                 v.lastClickTime = time.Now()
486
487                                                 v.tripleClick = true
488                                                 v.doubleClick = false
489
490                                                 v.Cursor.SelectLine()
491                                         } else {
492                                                 // Double click
493                                                 v.lastClickTime = time.Now()
494
495                                                 v.doubleClick = true
496                                                 v.tripleClick = false
497
498                                                 v.Cursor.SelectWord()
499                                         }
500                                 } else {
501                                         v.doubleClick = false
502                                         v.tripleClick = false
503                                         v.lastClickTime = time.Now()
504
505                                         v.Cursor.OrigSelection[0] = v.Cursor.Loc
506                                         v.Cursor.CurSelection[0] = v.Cursor.Loc
507                                         v.Cursor.CurSelection[1] = v.Cursor.Loc
508                                 }
509                                 v.mouseReleased = false
510                         } else if !v.mouseReleased {
511                                 v.MoveToMouseClick(x, y)
512                                 if v.tripleClick {
513                                         v.Cursor.AddLineToSelection()
514                                 } else if v.doubleClick {
515                                         v.Cursor.AddWordToSelection()
516                                 } else {
517                                         v.Cursor.SetSelectionEnd(v.Cursor.Loc)
518                                 }
519                         }
520                 case tcell.Button2:
521                         // Middle mouse button was clicked,
522                         // We should paste primary
523                         v.PastePrimary(true)
524                 case tcell.ButtonNone:
525                         // Mouse event with no click
526                         if !v.mouseReleased {
527                                 // Mouse was just released
528
529                                 // Relocating here isn't really necessary because the cursor will
530                                 // be in the right place from the last mouse event
531                                 // However, if we are running in a terminal that doesn't support mouse motion
532                                 // events, this still allows the user to make selections, except only after they
533                                 // release the mouse
534
535                                 if !v.doubleClick && !v.tripleClick {
536                                         v.MoveToMouseClick(x, y)
537                                         v.Cursor.SetSelectionEnd(v.Cursor.Loc)
538                                 }
539                                 v.mouseReleased = true
540                         }
541                 case tcell.WheelUp:
542                         // Scroll up
543                         scrollspeed := int(v.Buf.Settings["scrollspeed"].(float64))
544                         v.ScrollUp(scrollspeed)
545                 case tcell.WheelDown:
546                         // Scroll down
547                         scrollspeed := int(v.Buf.Settings["scrollspeed"].(float64))
548                         v.ScrollDown(scrollspeed)
549                 }
550         }
551
552         if relocate {
553                 v.Relocate()
554         }
555 }
556
557 // GutterMessage creates a message in this view's gutter
558 func (v *View) GutterMessage(section string, lineN int, msg string, kind int) {
559         lineN--
560         gutterMsg := GutterMessage{
561                 lineNum: lineN,
562                 msg:     msg,
563                 kind:    kind,
564         }
565         for _, v := range v.messages {
566                 for _, gmsg := range v {
567                         if gmsg.lineNum == lineN {
568                                 return
569                         }
570                 }
571         }
572         messages := v.messages[section]
573         v.messages[section] = append(messages, gutterMsg)
574 }
575
576 // ClearGutterMessages clears all gutter messages from a given section
577 func (v *View) ClearGutterMessages(section string) {
578         v.messages[section] = []GutterMessage{}
579 }
580
581 // ClearAllGutterMessages clears all the gutter messages
582 func (v *View) ClearAllGutterMessages() {
583         for k := range v.messages {
584                 v.messages[k] = []GutterMessage{}
585         }
586 }
587
588 // Opens the given help page in a new horizontal split
589 func (v *View) openHelp(helpPage string) {
590         if data, err := FindRuntimeFile(RTHelp, helpPage).Data(); err != nil {
591                 TermMessage("Unable to load help text", helpPage, "\n", err)
592         } else {
593                 helpBuffer := NewBuffer(data, helpPage+".md")
594                 helpBuffer.Name = "Help"
595
596                 if v.Type == vtHelp {
597                         v.OpenBuffer(helpBuffer)
598                 } else {
599                         v.HSplit(helpBuffer)
600                         CurView().Type = vtHelp
601                 }
602         }
603 }
604
605 func (v *View) drawCell(x, y int, ch rune, combc []rune, style tcell.Style) {
606         if x >= v.x && x < v.x+v.width && y >= v.y && y < v.y+v.height {
607                 screen.SetContent(x, y, ch, combc, style)
608         }
609 }
610
611 // DisplayView renders the view to the screen
612 func (v *View) DisplayView() {
613         if v.Type == vtLog {
614                 // Log views should always follow the cursor...
615                 v.Relocate()
616         }
617
618         if v.Buf.Settings["syntax"].(bool) {
619                 v.matches = Match(v)
620         }
621
622         // The charNum we are currently displaying
623         // starts at the start of the viewport
624         charNum := Loc{0, v.Topline}
625
626         // Convert the length of buffer to a string, and get the length of the string
627         // We are going to have to offset by that amount
628         maxLineLength := len(strconv.Itoa(v.Buf.NumLines))
629
630         if v.Buf.Settings["ruler"] == true {
631                 // + 1 for the little space after the line number
632                 v.lineNumOffset = maxLineLength + 1
633         } else {
634                 v.lineNumOffset = 0
635         }
636
637         // We need to add to the line offset if there are gutter messages
638         var hasGutterMessages bool
639         for _, v := range v.messages {
640                 if len(v) > 0 {
641                         hasGutterMessages = true
642                 }
643         }
644         if hasGutterMessages {
645                 v.lineNumOffset += 2
646         }
647
648         if v.x != 0 {
649                 // One space for the extra split divider
650                 v.lineNumOffset++
651         }
652
653         // These represent the current screen coordinates
654         screenX, screenY := v.x, v.y-1
655
656         highlightStyle := defStyle
657         curLineN := 0
658
659         // ViewLine is the current line from the top of the viewport
660         for viewLine := 0; viewLine < v.height; viewLine++ {
661                 screenY++
662                 screenX = v.x
663
664                 // This is the current line number of the buffer that we are drawing
665                 curLineN = viewLine + v.Topline
666
667                 if screenY-v.y >= v.height {
668                         break
669                 }
670
671                 if v.x != 0 {
672                         // Draw the split divider
673                         v.drawCell(screenX, screenY, '|', nil, defStyle.Reverse(true))
674                         screenX++
675                 }
676
677                 // If the buffer is smaller than the view height we have to clear all this space
678                 if curLineN >= v.Buf.NumLines {
679                         for i := screenX; i < v.x+v.width; i++ {
680                                 v.drawCell(i, screenY, ' ', nil, defStyle)
681                         }
682
683                         continue
684                 }
685                 line := v.Buf.Line(curLineN)
686
687                 // If there are gutter messages we need to display the '>>' symbol here
688                 if hasGutterMessages {
689                         // msgOnLine stores whether or not there is a gutter message on this line in particular
690                         msgOnLine := false
691                         for k := range v.messages {
692                                 for _, msg := range v.messages[k] {
693                                         if msg.lineNum == curLineN {
694                                                 msgOnLine = true
695                                                 gutterStyle := defStyle
696                                                 switch msg.kind {
697                                                 case GutterInfo:
698                                                         if style, ok := colorscheme["gutter-info"]; ok {
699                                                                 gutterStyle = style
700                                                         }
701                                                 case GutterWarning:
702                                                         if style, ok := colorscheme["gutter-warning"]; ok {
703                                                                 gutterStyle = style
704                                                         }
705                                                 case GutterError:
706                                                         if style, ok := colorscheme["gutter-error"]; ok {
707                                                                 gutterStyle = style
708                                                         }
709                                                 }
710                                                 v.drawCell(screenX, screenY, '>', nil, gutterStyle)
711                                                 screenX++
712                                                 v.drawCell(screenX, screenY, '>', nil, gutterStyle)
713                                                 screenX++
714                                                 if v.Cursor.Y == curLineN && !messenger.hasPrompt {
715                                                         messenger.Message(msg.msg)
716                                                         messenger.gutterMessage = true
717                                                 }
718                                         }
719                                 }
720                         }
721                         // If there is no message on this line we just display an empty offset
722                         if !msgOnLine {
723                                 v.drawCell(screenX, screenY, ' ', nil, defStyle)
724                                 screenX++
725                                 v.drawCell(screenX, screenY, ' ', nil, defStyle)
726                                 screenX++
727                                 if v.Cursor.Y == curLineN && messenger.gutterMessage {
728                                         messenger.Reset()
729                                         messenger.gutterMessage = false
730                                 }
731                         }
732                 }
733
734                 lineNumStyle := defStyle
735                 if v.Buf.Settings["ruler"] == true {
736                         // Write the line number
737                         if style, ok := colorscheme["line-number"]; ok {
738                                 lineNumStyle = style
739                         }
740                         if style, ok := colorscheme["current-line-number"]; ok {
741                                 if curLineN == v.Cursor.Y && tabs[curTab].curView == v.Num && !v.Cursor.HasSelection() {
742                                         lineNumStyle = style
743                                 }
744                         }
745
746                         lineNum := strconv.Itoa(curLineN + 1)
747
748                         // Write the spaces before the line number if necessary
749                         for i := 0; i < maxLineLength-len(lineNum); i++ {
750                                 v.drawCell(screenX, screenY, ' ', nil, lineNumStyle)
751                                 screenX++
752                         }
753                         // Write the actual line number
754                         for _, ch := range lineNum {
755                                 v.drawCell(screenX, screenY, ch, nil, lineNumStyle)
756                                 screenX++
757                         }
758
759                         // Write the extra space
760                         v.drawCell(screenX, screenY, ' ', nil, lineNumStyle)
761                         screenX++
762                 }
763
764                 // Now we actually draw the line
765                 colN := 0
766                 strWidth := 0
767                 tabSize := int(v.Buf.Settings["tabsize"].(float64))
768                 for _, ch := range line {
769                         if v.Buf.Settings["softwrap"].(bool) {
770                                 if screenX-v.x >= v.width {
771                                         screenY++
772                                         for i := 0; i < v.lineNumOffset; i++ {
773                                                 screen.SetContent(v.x+i, screenY, ' ', nil, lineNumStyle)
774                                         }
775                                         screenX = v.x + v.lineNumOffset
776                                 }
777                         }
778
779                         if tabs[curTab].curView == v.Num && !v.Cursor.HasSelection() && v.Cursor.Y == curLineN && colN == v.Cursor.X {
780                                 v.DisplayCursor(screenX-v.leftCol, screenY)
781                         }
782
783                         lineStyle := defStyle
784
785                         if v.Buf.Settings["syntax"].(bool) {
786                                 // Syntax highlighting is enabled
787                                 highlightStyle = v.matches[viewLine][colN]
788                         }
789
790                         if v.Cursor.HasSelection() &&
791                                 (charNum.GreaterEqual(v.Cursor.CurSelection[0]) && charNum.LessThan(v.Cursor.CurSelection[1]) ||
792                                         charNum.LessThan(v.Cursor.CurSelection[0]) && charNum.GreaterEqual(v.Cursor.CurSelection[1])) {
793                                 // The current character is selected
794                                 lineStyle = defStyle.Reverse(true)
795
796                                 if style, ok := colorscheme["selection"]; ok {
797                                         lineStyle = style
798                                 }
799                         } else {
800                                 lineStyle = highlightStyle
801                         }
802
803                         // We need to display the background of the linestyle with the correct color if cursorline is enabled
804                         // and this is the current view and there is no selection on this line and the cursor is on this line
805                         if v.Buf.Settings["cursorline"].(bool) && tabs[curTab].curView == v.Num && !v.Cursor.HasSelection() && v.Cursor.Y == curLineN {
806                                 if style, ok := colorscheme["cursor-line"]; ok {
807                                         fg, _, _ := style.Decompose()
808                                         lineStyle = lineStyle.Background(fg)
809                                 }
810                         }
811
812                         if ch == '\t' {
813                                 // If the character we are displaying is a tab, we need to do a bunch of special things
814
815                                 // First the user may have configured an `indent-char` to be displayed to show that this
816                                 // is a tab character
817                                 lineIndentStyle := defStyle
818                                 if style, ok := colorscheme["indent-char"]; ok && v.Buf.Settings["indentchar"].(string) != " " {
819                                         lineIndentStyle = style
820                                 }
821                                 if v.Cursor.HasSelection() &&
822                                         (charNum.GreaterEqual(v.Cursor.CurSelection[0]) && charNum.LessThan(v.Cursor.CurSelection[1]) ||
823                                                 charNum.LessThan(v.Cursor.CurSelection[0]) && charNum.GreaterEqual(v.Cursor.CurSelection[1])) {
824
825                                         lineIndentStyle = defStyle.Reverse(true)
826
827                                         if style, ok := colorscheme["selection"]; ok {
828                                                 lineIndentStyle = style
829                                         }
830                                 }
831                                 if v.Buf.Settings["cursorline"].(bool) && tabs[curTab].curView == v.Num && !v.Cursor.HasSelection() && v.Cursor.Y == curLineN {
832                                         if style, ok := colorscheme["cursor-line"]; ok {
833                                                 fg, _, _ := style.Decompose()
834                                                 lineIndentStyle = lineIndentStyle.Background(fg)
835                                         }
836                                 }
837                                 // Here we get the indent char
838                                 indentChar := []rune(v.Buf.Settings["indentchar"].(string))
839                                 if screenX-v.x-v.leftCol >= v.lineNumOffset {
840                                         v.drawCell(screenX-v.leftCol, screenY, indentChar[0], nil, lineIndentStyle)
841                                 }
842                                 // Now the tab has to be displayed as a bunch of spaces
843                                 visLoc := strWidth
844                                 remainder := tabSize - (visLoc % tabSize)
845                                 for i := 0; i < remainder-1; i++ {
846                                         screenX++
847                                         if screenX-v.x-v.leftCol >= v.lineNumOffset {
848                                                 v.drawCell(screenX-v.leftCol, screenY, ' ', nil, lineStyle)
849                                         }
850                                 }
851                         } else if runewidth.RuneWidth(ch) > 1 {
852                                 if screenX-v.x-v.leftCol >= v.lineNumOffset {
853                                         v.drawCell(screenX, screenY, ch, nil, lineStyle)
854                                 }
855                                 for i := 0; i < runewidth.RuneWidth(ch)-1; i++ {
856                                         screenX++
857                                         if screenX-v.x-v.leftCol >= v.lineNumOffset {
858                                                 v.drawCell(screenX-v.leftCol, screenY, '<', nil, lineStyle)
859                                         }
860                                 }
861                         } else {
862                                 if screenX-v.x-v.leftCol >= v.lineNumOffset {
863                                         v.drawCell(screenX-v.leftCol, screenY, ch, nil, lineStyle)
864                                 }
865                         }
866                         charNum = charNum.Move(1, v.Buf)
867                         screenX++
868                         colN++
869                         strWidth += StringWidth(string(ch), tabSize)
870                 }
871                 // Here we are at a newline
872
873                 if tabs[curTab].curView == v.Num && !v.Cursor.HasSelection() && v.Cursor.Y == curLineN && colN == v.Cursor.X {
874                         v.DisplayCursor(screenX-v.leftCol, screenY)
875                 }
876
877                 // The newline may be selected, in which case we should draw the selection style
878                 // with a space to represent it
879                 if v.Cursor.HasSelection() &&
880                         (charNum.GreaterEqual(v.Cursor.CurSelection[0]) && charNum.LessThan(v.Cursor.CurSelection[1]) ||
881                                 charNum.LessThan(v.Cursor.CurSelection[0]) && charNum.GreaterEqual(v.Cursor.CurSelection[1])) {
882
883                         selectStyle := defStyle.Reverse(true)
884
885                         if style, ok := colorscheme["selection"]; ok {
886                                 selectStyle = style
887                         }
888                         v.drawCell(screenX, screenY, ' ', nil, selectStyle)
889                         screenX++
890                 }
891
892                 charNum = charNum.Move(1, v.Buf)
893
894                 for i := 0; i < v.width; i++ {
895                         lineStyle := defStyle
896                         if v.Buf.Settings["cursorline"].(bool) && tabs[curTab].curView == v.Num && !v.Cursor.HasSelection() && v.Cursor.Y == curLineN {
897                                 if style, ok := colorscheme["cursor-line"]; ok {
898                                         fg, _, _ := style.Decompose()
899                                         lineStyle = lineStyle.Background(fg)
900                                 }
901                         }
902                         if screenX-v.x-v.leftCol+i >= v.lineNumOffset {
903                                 colorcolumn := int(v.Buf.Settings["colorcolumn"].(float64))
904                                 if colorcolumn != 0 && screenX-v.lineNumOffset+i == colorcolumn-1 {
905                                         if style, ok := colorscheme["color-column"]; ok {
906                                                 fg, _, _ := style.Decompose()
907                                                 lineStyle = lineStyle.Background(fg)
908                                         }
909                                 }
910                                 v.drawCell(screenX-v.leftCol+i, screenY, ' ', nil, lineStyle)
911                         }
912                 }
913         }
914         v.Bottomline = curLineN
915         if !v.Buf.Settings["softwrap"].(bool) {
916                 v.Bottomline++
917         }
918 }
919
920 // DisplayCursor draws the current buffer's cursor to the screen
921 func (v *View) DisplayCursor(x, y int) {
922         // screen.ShowCursor(v.x+v.Cursor.GetVisualX()+v.lineNumOffset-v.leftCol, y)
923         screen.ShowCursor(x, y)
924 }
925
926 // Display renders the view, the cursor, and statusline
927 func (v *View) Display() {
928         v.DisplayView()
929         // Don't draw the cursor if it is out of the viewport or if it has a selection
930         if (v.Cursor.Y-v.Topline < 0 || v.Cursor.Y-v.Topline > v.height-1) || v.Cursor.HasSelection() {
931                 screen.HideCursor()
932         }
933         _, screenH := screen.Size()
934         if v.Buf.Settings["statusline"].(bool) {
935                 v.sline.Display()
936         } else if (v.y + v.height) != screenH-1 {
937                 for x := 0; x < v.width; x++ {
938                         screen.SetContent(v.x+x, v.y+v.height, '-', nil, defStyle.Reverse(true))
939                 }
940         }
941 }