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