]> git.lizzy.rs Git - micro.git/blob - cmd/micro/view.go
Only register double click on equal mouse location
[micro.git] / cmd / micro / view.go
1 package main
2
3 import (
4         "os"
5         "strconv"
6         "strings"
7         "time"
8
9         "github.com/mitchellh/go-homedir"
10         "github.com/zyedidia/tcell"
11 )
12
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.IsModified {
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                                 return true
215                         } else {
216                                 return true
217                         }
218                 }
219         } else {
220                 return true
221         }
222         return false
223 }
224
225 // OpenBuffer opens a new buffer in this view.
226 // This resets the topline, event handler and cursor.
227 func (v *View) OpenBuffer(buf *Buffer) {
228         screen.Clear()
229         v.CloseBuffer()
230         v.Buf = buf
231         v.Cursor = &buf.Cursor
232         v.Topline = 0
233         v.leftCol = 0
234         v.Cursor.ResetSelection()
235         v.Relocate()
236         v.Center(false)
237         v.messages = make(map[string][]GutterMessage)
238
239         // Set mouseReleased to true because we assume the mouse is not being pressed when
240         // the editor is opened
241         v.mouseReleased = true
242         v.lastClickTime = time.Time{}
243 }
244
245 // Open opens the given file in the view
246 func (v *View) Open(filename string) {
247         home, _ := homedir.Dir()
248         filename = strings.Replace(filename, "~", home, 1)
249         file, err := os.Open(filename)
250         fileInfo, _ := os.Stat(filename)
251
252         if err == nil && fileInfo.IsDir() {
253                 messenger.Error(filename, " is a directory")
254                 return
255         }
256
257         defer file.Close()
258
259         var buf *Buffer
260         if err != nil {
261                 messenger.Message(err.Error())
262                 // File does not exist -- create an empty buffer with that name
263                 buf = NewBufferFromString("", filename)
264         } else {
265                 buf = NewBuffer(file, FSize(file), filename)
266         }
267         v.OpenBuffer(buf)
268 }
269
270 // CloseBuffer performs any closing functions on the buffer
271 func (v *View) CloseBuffer() {
272         if v.Buf != nil {
273                 v.Buf.Serialize()
274         }
275 }
276
277 // ReOpen reloads the current buffer
278 func (v *View) ReOpen() {
279         if v.CanClose() {
280                 screen.Clear()
281                 v.Buf.ReOpen()
282                 v.Relocate()
283         }
284 }
285
286 // HSplit opens a horizontal split with the given buffer
287 func (v *View) HSplit(buf *Buffer) {
288         i := 0
289         if v.Buf.Settings["splitBottom"].(bool) {
290                 i = 1
291         }
292         v.splitNode.HSplit(buf, v.Num+i)
293 }
294
295 // VSplit opens a vertical split with the given buffer
296 func (v *View) VSplit(buf *Buffer) {
297         i := 0
298         if v.Buf.Settings["splitRight"].(bool) {
299                 i = 1
300         }
301         v.splitNode.VSplit(buf, v.Num+i)
302 }
303
304 // HSplitIndex opens a horizontal split with the given buffer at the given index
305 func (v *View) HSplitIndex(buf *Buffer, splitIndex int) {
306         v.splitNode.HSplit(buf, splitIndex)
307 }
308
309 // VSplitIndex opens a vertical split with the given buffer at the given index
310 func (v *View) VSplitIndex(buf *Buffer, splitIndex int) {
311         v.splitNode.VSplit(buf, splitIndex)
312 }
313
314 // GetSoftWrapLocation gets the location of a visual click on the screen and converts it to col,line
315 func (v *View) GetSoftWrapLocation(vx, vy int) (int, int) {
316         if !v.Buf.Settings["softwrap"].(bool) {
317                 if vy >= v.Buf.NumLines {
318                         vy = v.Buf.NumLines - 1
319                 }
320                 vx = v.Cursor.GetCharPosInLine(vy, vx)
321                 return vx, vy
322         }
323
324         screenX, screenY := 0, v.Topline
325         for lineN := v.Topline; lineN < v.Bottomline(); lineN++ {
326                 line := v.Buf.Line(lineN)
327                 if lineN >= v.Buf.NumLines {
328                         return 0, v.Buf.NumLines - 1
329                 }
330
331                 colN := 0
332                 for _, ch := range line {
333                         if screenX >= v.Width-v.lineNumOffset {
334                                 screenX = 0
335                                 screenY++
336                         }
337
338                         if screenX == vx && screenY == vy {
339                                 return colN, lineN
340                         }
341
342                         if ch == '\t' {
343                                 screenX += int(v.Buf.Settings["tabsize"].(float64)) - 1
344                         }
345
346                         screenX++
347                         colN++
348                 }
349                 if screenY == vy {
350                         return colN, lineN
351                 }
352                 screenX = 0
353                 screenY++
354         }
355
356         return 0, 0
357 }
358
359 func (v *View) Bottomline() int {
360         if !v.Buf.Settings["softwrap"].(bool) {
361                 return v.Topline + v.Height
362         }
363
364         screenX, screenY := 0, 0
365         numLines := 0
366         for lineN := v.Topline; lineN < v.Topline+v.Height; lineN++ {
367                 line := v.Buf.Line(lineN)
368
369                 colN := 0
370                 for _, ch := range line {
371                         if screenX >= v.Width-v.lineNumOffset {
372                                 screenX = 0
373                                 screenY++
374                         }
375
376                         if ch == '\t' {
377                                 screenX += int(v.Buf.Settings["tabsize"].(float64)) - 1
378                         }
379
380                         screenX++
381                         colN++
382                 }
383                 screenX = 0
384                 screenY++
385                 numLines++
386
387                 if screenY >= v.Height {
388                         break
389                 }
390         }
391         return numLines + v.Topline
392 }
393
394 // Relocate moves the view window so that the cursor is in view
395 // This is useful if the user has scrolled far away, and then starts typing
396 func (v *View) Relocate() bool {
397         height := v.Bottomline() - v.Topline
398         ret := false
399         cy := v.Cursor.Y
400         scrollmargin := int(v.Buf.Settings["scrollmargin"].(float64))
401         if cy < v.Topline+scrollmargin && cy > scrollmargin-1 {
402                 v.Topline = cy - scrollmargin
403                 ret = true
404         } else if cy < v.Topline {
405                 v.Topline = cy
406                 ret = true
407         }
408         if cy > v.Topline+height-1-scrollmargin && cy < v.Buf.NumLines-scrollmargin {
409                 v.Topline = cy - height + 1 + scrollmargin
410                 ret = true
411         } else if cy >= v.Buf.NumLines-scrollmargin && cy > height {
412                 v.Topline = v.Buf.NumLines - height
413                 ret = true
414         }
415
416         if !v.Buf.Settings["softwrap"].(bool) {
417                 cx := v.Cursor.GetVisualX()
418                 if cx < v.leftCol {
419                         v.leftCol = cx
420                         ret = true
421                 }
422                 if cx+v.lineNumOffset+1 > v.leftCol+v.Width {
423                         v.leftCol = cx - v.Width + v.lineNumOffset + 1
424                         ret = true
425                 }
426         }
427         return ret
428 }
429
430 // MoveToMouseClick moves the cursor to location x, y assuming x, y were given
431 // by a mouse click
432 func (v *View) MoveToMouseClick(x, y int) {
433         if y-v.Topline > v.Height-1 {
434                 v.ScrollDown(1)
435                 y = v.Height + v.Topline - 1
436         }
437         if y < 0 {
438                 y = 0
439         }
440         if x < 0 {
441                 x = 0
442         }
443
444         x, y = v.GetSoftWrapLocation(x, y)
445         // x = v.Cursor.GetCharPosInLine(y, x)
446         if x > Count(v.Buf.Line(y)) {
447                 x = Count(v.Buf.Line(y))
448         }
449         v.Cursor.X = x
450         v.Cursor.Y = y
451         v.Cursor.LastVisualX = v.Cursor.GetVisualX()
452 }
453
454 func (v *View) ExecuteActions(actions []func(*View, bool) bool) bool {
455         relocate := false
456         readonlyBindingsList := []string{"Delete", "Insert", "Backspace", "Cut", "Play", "Paste", "Move", "Add", "DuplicateLine", "Macro"}
457         for _, action := range actions {
458                 readonlyBindingsResult := false
459                 funcName := ShortFuncName(action)
460                 if v.Type.readonly == true {
461                         // check for readonly and if true only let key bindings get called if they do not change the contents.
462                         for _, readonlyBindings := range readonlyBindingsList {
463                                 if strings.Contains(funcName, readonlyBindings) {
464                                         readonlyBindingsResult = true
465                                 }
466                         }
467                 }
468                 if !readonlyBindingsResult {
469                         // call the key binding
470                         relocate = action(v, true) || relocate
471                         // Macro
472                         if funcName != "ToggleMacro" && funcName != "PlayMacro" {
473                                 if recordingMacro {
474                                         curMacro = append(curMacro, action)
475                                 }
476                         }
477                 }
478         }
479
480         return relocate
481 }
482
483 func (v *View) SetCursor(c *Cursor) bool {
484         if c == nil {
485                 return false
486         }
487         v.Cursor = c
488         v.Buf.curCursor = c.Num
489
490         return true
491 }
492
493 // HandleEvent handles an event passed by the main loop
494 func (v *View) HandleEvent(event tcell.Event) {
495         // This bool determines whether the view is relocated at the end of the function
496         // By default it's true because most events should cause a relocate
497         relocate := true
498
499         v.Buf.CheckModTime()
500
501         switch e := event.(type) {
502         case *tcell.EventKey:
503                 // Check first if input is a key binding, if it is we 'eat' the input and don't insert a rune
504                 isBinding := false
505                 for key, actions := range bindings {
506                         if e.Key() == key.keyCode {
507                                 if e.Key() == tcell.KeyRune {
508                                         if e.Rune() != key.r {
509                                                 continue
510                                         }
511                                 }
512                                 if e.Modifiers() == key.modifiers {
513                                         for _, c := range v.Buf.cursors {
514                                                 ok := v.SetCursor(c)
515                                                 if !ok {
516                                                         break
517                                                 }
518                                                 relocate = false
519                                                 isBinding = true
520                                                 relocate = v.ExecuteActions(actions) || relocate
521                                         }
522                                         v.SetCursor(&v.Buf.Cursor)
523                                         v.Buf.MergeCursors()
524                                         break
525                                 }
526                         }
527                 }
528                 if !isBinding && e.Key() == tcell.KeyRune {
529                         // Check viewtype if readonly don't insert a rune (readonly help and log view etc.)
530                         if v.Type.readonly == false {
531                                 for _, c := range v.Buf.cursors {
532                                         v.SetCursor(c)
533
534                                         // Insert a character
535                                         if v.Cursor.HasSelection() {
536                                                 v.Cursor.DeleteSelection()
537                                                 v.Cursor.ResetSelection()
538                                         }
539                                         v.Buf.Insert(v.Cursor.Loc, string(e.Rune()))
540
541                                         for pl := range loadedPlugins {
542                                                 _, err := Call(pl+".onRune", string(e.Rune()), v)
543                                                 if err != nil && !strings.HasPrefix(err.Error(), "function does not exist") {
544                                                         TermMessage(err)
545                                                 }
546                                         }
547
548                                         if recordingMacro {
549                                                 curMacro = append(curMacro, e.Rune())
550                                         }
551                                 }
552                                 v.SetCursor(&v.Buf.Cursor)
553                         }
554                 }
555         case *tcell.EventPaste:
556                 // Check viewtype if readonly don't paste (readonly help and log view etc.)
557                 if v.Type.readonly == false {
558                         if !PreActionCall("Paste", v) {
559                                 break
560                         }
561
562                         for _, c := range v.Buf.cursors {
563                                 v.SetCursor(c)
564                                 v.paste(e.Text())
565                         }
566                         v.SetCursor(&v.Buf.Cursor)
567
568                         PostActionCall("Paste", v)
569                 }
570         case *tcell.EventMouse:
571                 // Don't relocate for mouse events
572                 relocate = false
573
574                 button := e.Buttons()
575
576                 for key, actions := range bindings {
577                         if button == key.buttons && e.Modifiers() == key.modifiers {
578                                 for _, c := range v.Buf.cursors {
579                                         ok := v.SetCursor(c)
580                                         if !ok {
581                                                 break
582                                         }
583                                         relocate = v.ExecuteActions(actions) || relocate
584                                 }
585                                 v.SetCursor(&v.Buf.Cursor)
586                                 v.Buf.MergeCursors()
587                         }
588                 }
589
590                 for key, actions := range mouseBindings {
591                         if button == key.buttons && e.Modifiers() == key.modifiers {
592                                 for _, action := range actions {
593                                         action(v, true, e)
594                                 }
595                         }
596                 }
597
598                 switch button {
599                 case tcell.ButtonNone:
600                         // Mouse event with no click
601                         if !v.mouseReleased {
602                                 // Mouse was just released
603
604                                 x, y := e.Position()
605                                 x -= v.lineNumOffset - v.leftCol + v.x
606                                 y += v.Topline - v.y
607
608                                 // Relocating here isn't really necessary because the cursor will
609                                 // be in the right place from the last mouse event
610                                 // However, if we are running in a terminal that doesn't support mouse motion
611                                 // events, this still allows the user to make selections, except only after they
612                                 // release the mouse
613
614                                 if !v.doubleClick && !v.tripleClick {
615                                         v.MoveToMouseClick(x, y)
616                                         v.Cursor.SetSelectionEnd(v.Cursor.Loc)
617                                         v.Cursor.CopySelection("primary")
618                                 }
619                                 v.mouseReleased = true
620                         }
621                 }
622         }
623
624         if relocate {
625                 v.Relocate()
626                 // We run relocate again because there's a bug with relocating with softwrap
627                 // when for example you jump to the bottom of the buffer and it tries to
628                 // calculate where to put the topline so that the bottom line is at the bottom
629                 // of the terminal and it runs into problems with visual lines vs real lines.
630                 // This is (hopefully) a temporary solution
631                 v.Relocate()
632         }
633 }
634
635 func (v *View) mainCursor() bool {
636         return v.Buf.curCursor == len(v.Buf.cursors)-1
637 }
638
639 // GutterMessage creates a message in this view's gutter
640 func (v *View) GutterMessage(section string, lineN int, msg string, kind int) {
641         lineN--
642         gutterMsg := GutterMessage{
643                 lineNum: lineN,
644                 msg:     msg,
645                 kind:    kind,
646         }
647         for _, v := range v.messages {
648                 for _, gmsg := range v {
649                         if gmsg.lineNum == lineN {
650                                 return
651                         }
652                 }
653         }
654         messages := v.messages[section]
655         v.messages[section] = append(messages, gutterMsg)
656 }
657
658 // ClearGutterMessages clears all gutter messages from a given section
659 func (v *View) ClearGutterMessages(section string) {
660         v.messages[section] = []GutterMessage{}
661 }
662
663 // ClearAllGutterMessages clears all the gutter messages
664 func (v *View) ClearAllGutterMessages() {
665         for k := range v.messages {
666                 v.messages[k] = []GutterMessage{}
667         }
668 }
669
670 // Opens the given help page in a new horizontal split
671 func (v *View) openHelp(helpPage string) {
672         if data, err := FindRuntimeFile(RTHelp, helpPage).Data(); err != nil {
673                 TermMessage("Unable to load help text", helpPage, "\n", err)
674         } else {
675                 helpBuffer := NewBufferFromString(string(data), helpPage+".md")
676                 helpBuffer.name = "Help"
677
678                 if v.Type == vtHelp {
679                         v.OpenBuffer(helpBuffer)
680                 } else {
681                         v.HSplit(helpBuffer)
682                         CurView().Type = vtHelp
683                 }
684         }
685 }
686
687 func (v *View) DisplayView() {
688         if v.Buf.Settings["softwrap"].(bool) && v.leftCol != 0 {
689                 v.leftCol = 0
690         }
691
692         if v.Type == vtLog {
693                 // Log views should always follow the cursor...
694                 v.Relocate()
695         }
696
697         // We need to know the string length of the largest line number
698         // so we can pad appropriately when displaying line numbers
699         maxLineNumLength := len(strconv.Itoa(v.Buf.NumLines))
700
701         if v.Buf.Settings["ruler"] == true {
702                 // + 1 for the little space after the line number
703                 v.lineNumOffset = maxLineNumLength + 1
704         } else {
705                 v.lineNumOffset = 0
706         }
707
708         // We need to add to the line offset if there are gutter messages
709         var hasGutterMessages bool
710         for _, v := range v.messages {
711                 if len(v) > 0 {
712                         hasGutterMessages = true
713                 }
714         }
715         if hasGutterMessages {
716                 v.lineNumOffset += 2
717         }
718
719         divider := 0
720         if v.x != 0 {
721                 // One space for the extra split divider
722                 v.lineNumOffset++
723                 divider = 1
724         }
725
726         xOffset := v.x + v.lineNumOffset
727         yOffset := v.y
728
729         height := v.Height
730         width := v.Width
731         left := v.leftCol
732         top := v.Topline
733
734         v.cellview.Draw(v.Buf, top, height, left, width-v.lineNumOffset)
735
736         screenX := v.x
737         realLineN := top - 1
738         visualLineN := 0
739         var line []*Char
740         for visualLineN, line = range v.cellview.lines {
741                 var firstChar *Char
742                 if len(line) > 0 {
743                         firstChar = line[0]
744                 }
745
746                 var softwrapped bool
747                 if firstChar != nil {
748                         if firstChar.realLoc.Y == realLineN {
749                                 softwrapped = true
750                         }
751                         realLineN = firstChar.realLoc.Y
752                 } else {
753                         realLineN++
754                 }
755
756                 colorcolumn := int(v.Buf.Settings["colorcolumn"].(float64))
757                 if colorcolumn != 0 {
758                         style := GetColor("color-column")
759                         fg, _, _ := style.Decompose()
760                         st := defStyle.Background(fg)
761                         screen.SetContent(xOffset+colorcolumn, yOffset+visualLineN, ' ', nil, st)
762                 }
763
764                 screenX = v.x
765
766                 // If there are gutter messages we need to display the '>>' symbol here
767                 if hasGutterMessages {
768                         // msgOnLine stores whether or not there is a gutter message on this line in particular
769                         msgOnLine := false
770                         for k := range v.messages {
771                                 for _, msg := range v.messages[k] {
772                                         if msg.lineNum == realLineN {
773                                                 msgOnLine = true
774                                                 gutterStyle := defStyle
775                                                 switch msg.kind {
776                                                 case GutterInfo:
777                                                         if style, ok := colorscheme["gutter-info"]; ok {
778                                                                 gutterStyle = style
779                                                         }
780                                                 case GutterWarning:
781                                                         if style, ok := colorscheme["gutter-warning"]; ok {
782                                                                 gutterStyle = style
783                                                         }
784                                                 case GutterError:
785                                                         if style, ok := colorscheme["gutter-error"]; ok {
786                                                                 gutterStyle = style
787                                                         }
788                                                 }
789                                                 screen.SetContent(screenX, yOffset+visualLineN, '>', nil, gutterStyle)
790                                                 screenX++
791                                                 screen.SetContent(screenX, yOffset+visualLineN, '>', nil, gutterStyle)
792                                                 screenX++
793                                                 if v.Cursor.Y == realLineN && !messenger.hasPrompt {
794                                                         messenger.Message(msg.msg)
795                                                         messenger.gutterMessage = true
796                                                 }
797                                         }
798                                 }
799                         }
800                         // If there is no message on this line we just display an empty offset
801                         if !msgOnLine {
802                                 screen.SetContent(screenX, yOffset+visualLineN, ' ', nil, defStyle)
803                                 screenX++
804                                 screen.SetContent(screenX, yOffset+visualLineN, ' ', nil, defStyle)
805                                 screenX++
806                                 if v.Cursor.Y == realLineN && messenger.gutterMessage {
807                                         messenger.Reset()
808                                         messenger.gutterMessage = false
809                                 }
810                         }
811                 }
812
813                 lineNumStyle := defStyle
814                 if v.Buf.Settings["ruler"] == true {
815                         // Write the line number
816                         if style, ok := colorscheme["line-number"]; ok {
817                                 lineNumStyle = style
818                         }
819                         if style, ok := colorscheme["current-line-number"]; ok {
820                                 if realLineN == v.Cursor.Y && tabs[curTab].CurView == v.Num && !v.Cursor.HasSelection() {
821                                         lineNumStyle = style
822                                 }
823                         }
824
825                         lineNum := strconv.Itoa(realLineN + 1)
826
827                         // Write the spaces before the line number if necessary
828                         for i := 0; i < maxLineNumLength-len(lineNum); i++ {
829                                 screen.SetContent(screenX+divider, yOffset+visualLineN, ' ', nil, lineNumStyle)
830                                 screenX++
831                         }
832                         if softwrapped && visualLineN != 0 {
833                                 // Pad without the line number because it was written on the visual line before
834                                 for range lineNum {
835                                         screen.SetContent(screenX+divider, yOffset+visualLineN, ' ', nil, lineNumStyle)
836                                         screenX++
837                                 }
838                         } else {
839                                 // Write the actual line number
840                                 for _, ch := range lineNum {
841                                         screen.SetContent(screenX+divider, yOffset+visualLineN, ch, nil, lineNumStyle)
842                                         screenX++
843                                 }
844                         }
845
846                         // Write the extra space
847                         screen.SetContent(screenX+divider, yOffset+visualLineN, ' ', nil, lineNumStyle)
848                         screenX++
849                 }
850
851                 var lastChar *Char
852                 cursorSet := false
853                 for _, char := range line {
854                         if char != nil {
855                                 lineStyle := char.style
856
857                                 colorcolumn := int(v.Buf.Settings["colorcolumn"].(float64))
858                                 if colorcolumn != 0 && char.visualLoc.X == colorcolumn {
859                                         style := GetColor("color-column")
860                                         fg, _, _ := style.Decompose()
861                                         lineStyle = lineStyle.Background(fg)
862                                 }
863
864                                 charLoc := char.realLoc
865                                 for _, c := range v.Buf.cursors {
866                                         v.SetCursor(c)
867                                         if v.Cursor.HasSelection() &&
868                                                 (charLoc.GreaterEqual(v.Cursor.CurSelection[0]) && charLoc.LessThan(v.Cursor.CurSelection[1]) ||
869                                                         charLoc.LessThan(v.Cursor.CurSelection[0]) && charLoc.GreaterEqual(v.Cursor.CurSelection[1])) {
870                                                 // The current character is selected
871                                                 lineStyle = defStyle.Reverse(true)
872
873                                                 if style, ok := colorscheme["selection"]; ok {
874                                                         lineStyle = style
875                                                 }
876                                         }
877                                 }
878                                 v.SetCursor(&v.Buf.Cursor)
879
880                                 if v.Buf.Settings["cursorline"].(bool) && tabs[curTab].CurView == v.Num &&
881                                         !v.Cursor.HasSelection() && v.Cursor.Y == realLineN {
882                                         style := GetColor("cursor-line")
883                                         fg, _, _ := style.Decompose()
884                                         lineStyle = lineStyle.Background(fg)
885                                 }
886
887                                 screen.SetContent(xOffset+char.visualLoc.X, yOffset+char.visualLoc.Y, char.drawChar, nil, lineStyle)
888
889                                 for i, c := range v.Buf.cursors {
890                                         v.SetCursor(c)
891                                         if tabs[curTab].CurView == v.Num && !v.Cursor.HasSelection() &&
892                                                 v.Cursor.Y == char.realLoc.Y && v.Cursor.X == char.realLoc.X && (!cursorSet || i != 0) {
893                                                 ShowMultiCursor(xOffset+char.visualLoc.X, yOffset+char.visualLoc.Y, i)
894                                                 cursorSet = true
895                                         }
896                                 }
897                                 v.SetCursor(&v.Buf.Cursor)
898
899                                 lastChar = char
900                         }
901                 }
902
903                 lastX := 0
904                 var realLoc Loc
905                 var visualLoc Loc
906                 var cx, cy int
907                 if lastChar != nil {
908                         lastX = xOffset + lastChar.visualLoc.X + lastChar.width
909                         for i, c := range v.Buf.cursors {
910                                 v.SetCursor(c)
911                                 if tabs[curTab].CurView == v.Num && !v.Cursor.HasSelection() &&
912                                         v.Cursor.Y == lastChar.realLoc.Y && v.Cursor.X == lastChar.realLoc.X+1 {
913                                         ShowMultiCursor(lastX, yOffset+lastChar.visualLoc.Y, i)
914                                         cx, cy = lastX, yOffset+lastChar.visualLoc.Y
915                                 }
916                         }
917                         v.SetCursor(&v.Buf.Cursor)
918                         realLoc = Loc{lastChar.realLoc.X + 1, realLineN}
919                         visualLoc = Loc{lastX - xOffset, lastChar.visualLoc.Y}
920                 } else if len(line) == 0 {
921                         for i, c := range v.Buf.cursors {
922                                 v.SetCursor(c)
923                                 if tabs[curTab].CurView == v.Num && !v.Cursor.HasSelection() &&
924                                         v.Cursor.Y == realLineN {
925                                         ShowMultiCursor(xOffset, yOffset+visualLineN, i)
926                                         cx, cy = xOffset, yOffset+visualLineN
927                                 }
928                         }
929                         v.SetCursor(&v.Buf.Cursor)
930                         lastX = xOffset
931                         realLoc = Loc{0, realLineN}
932                         visualLoc = Loc{0, visualLineN}
933                 }
934
935                 if v.Cursor.HasSelection() &&
936                         (realLoc.GreaterEqual(v.Cursor.CurSelection[0]) && realLoc.LessThan(v.Cursor.CurSelection[1]) ||
937                                 realLoc.LessThan(v.Cursor.CurSelection[0]) && realLoc.GreaterEqual(v.Cursor.CurSelection[1])) {
938                         // The current character is selected
939                         selectStyle := defStyle.Reverse(true)
940
941                         if style, ok := colorscheme["selection"]; ok {
942                                 selectStyle = style
943                         }
944                         screen.SetContent(xOffset+visualLoc.X, yOffset+visualLoc.Y, ' ', nil, selectStyle)
945                 }
946
947                 if v.Buf.Settings["cursorline"].(bool) && tabs[curTab].CurView == v.Num &&
948                         !v.Cursor.HasSelection() && v.Cursor.Y == realLineN {
949                         for i := lastX; i < xOffset+v.Width-v.lineNumOffset; i++ {
950                                 style := GetColor("cursor-line")
951                                 fg, _, _ := style.Decompose()
952                                 style = style.Background(fg)
953                                 if !(tabs[curTab].CurView == v.Num && !v.Cursor.HasSelection() && i == cx && yOffset+visualLineN == cy) {
954                                         screen.SetContent(i, yOffset+visualLineN, ' ', nil, style)
955                                 }
956                         }
957                 }
958         }
959
960         if divider != 0 {
961                 dividerStyle := defStyle
962                 if style, ok := colorscheme["divider"]; ok {
963                         dividerStyle = style
964                 }
965                 for i := 0; i < v.Height; i++ {
966                         screen.SetContent(v.x, yOffset+i, '|', nil, dividerStyle.Reverse(true))
967                 }
968         }
969 }
970
971 // ShowMultiCursor will display a cursor at a location
972 // If i == 0 then the terminal cursor will be used
973 // Otherwise a fake cursor will be drawn at the position
974 func ShowMultiCursor(x, y, i int) {
975         if i == 0 {
976                 screen.ShowCursor(x, y)
977         } else {
978                 r, _, _, _ := screen.GetContent(x, y)
979                 screen.SetContent(x, y, r, nil, defStyle.Reverse(true))
980         }
981 }
982
983 // Display renders the view, the cursor, and statusline
984 func (v *View) Display() {
985         if globalSettings["termtitle"].(bool) {
986                 screen.SetTitle("micro: " + v.Buf.GetName())
987         }
988         v.DisplayView()
989         // Don't draw the cursor if it is out of the viewport or if it has a selection
990         if v.Num == tabs[curTab].CurView && (v.Cursor.Y-v.Topline < 0 || v.Cursor.Y-v.Topline > v.Height-1 || v.Cursor.HasSelection()) {
991                 screen.HideCursor()
992         }
993         _, screenH := screen.Size()
994         if v.Buf.Settings["statusline"].(bool) {
995                 v.sline.Display()
996         } else if (v.y + v.Height) != screenH-1 {
997                 for x := 0; x < v.Width; x++ {
998                         screen.SetContent(v.x+x, v.y+v.Height, '-', nil, defStyle.Reverse(true))
999                 }
1000         }
1001 }