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