]> git.lizzy.rs Git - micro.git/blob - cmd/micro/view.go
Rename to tabstospaces for consistency
[micro.git] / cmd / micro / view.go
1 package main
2
3 import (
4         "io/ioutil"
5         "reflect"
6         "runtime"
7         "strconv"
8         "strings"
9         "time"
10
11         "github.com/zyedidia/tcell"
12 )
13
14 // The View struct stores information about a view into a buffer.
15 // It stores information about the cursor, and the viewport
16 // that the user sees the buffer from.
17 type View struct {
18         // A pointer to the buffer's cursor for ease of access
19         Cursor *Cursor
20
21         // The topmost line, used for vertical scrolling
22         Topline int
23         // The leftmost column, used for horizontal scrolling
24         leftCol int
25
26         // Percentage of the terminal window that this view takes up (from 0 to 100)
27         widthPercent  int
28         heightPercent int
29
30         // Actual with and height
31         width  int
32         height int
33
34         // How much to offset because of line numbers
35         lineNumOffset int
36
37         // Holds the list of gutter messages
38         messages map[string][]GutterMessage
39
40         // The buffer
41         Buf *Buffer
42         // The statusline
43         sline Statusline
44
45         // Since tcell doesn't differentiate between a mouse release event
46         // and a mouse move event with no keys pressed, we need to keep
47         // track of whether or not the mouse was pressed (or not released) last event to determine
48         // mouse release events
49         mouseReleased bool
50
51         // This stores when the last click was
52         // This is useful for detecting double and triple clicks
53         lastClickTime time.Time
54
55         // lastCutTime stores when the last ctrl+k was issued.
56         // It is used for clearing the clipboard to replace it with fresh cut lines.
57         lastCutTime time.Time
58
59         // freshClip returns true if the clipboard has never been pasted.
60         freshClip bool
61
62         // Was the last mouse event actually a double click?
63         // Useful for detecting triple clicks -- if a double click is detected
64         // but the last mouse event was actually a double click, it's a triple click
65         doubleClick bool
66         // Same here, just to keep track for mouse move events
67         tripleClick bool
68
69         // Syntax highlighting matches
70         matches SyntaxMatches
71         // The matches from the last frame
72         lastMatches SyntaxMatches
73 }
74
75 // NewView returns a new fullscreen view
76 func NewView(buf *Buffer) *View {
77         return NewViewWidthHeight(buf, 100, 100)
78 }
79
80 // NewViewWidthHeight returns a new view with the specified width and height percentages
81 // Note that w and h are percentages not actual values
82 func NewViewWidthHeight(buf *Buffer, w, h int) *View {
83         v := new(View)
84
85         v.widthPercent = w
86         v.heightPercent = h
87         v.Resize(screen.Size())
88
89         v.OpenBuffer(buf)
90
91         v.messages = make(map[string][]GutterMessage)
92
93         v.sline = Statusline{
94                 view: v,
95         }
96
97         return v
98 }
99
100 // Resize recalculates the actual width and height of the view from the width and height
101 // percentages
102 // This is usually called when the window is resized, or when a split has been added and
103 // the percentages have changed
104 func (v *View) Resize(w, h int) {
105         // Always include 1 line for the command line at the bottom
106         h--
107         v.width = int(float32(w) * float32(v.widthPercent) / 100)
108         // We subtract 1 for the statusline
109         v.height = int(float32(h) * float32(v.heightPercent) / 100)
110         if settings["statusline"].(bool) {
111                 // Make room for the status line if it is enabled
112                 v.height--
113         }
114 }
115
116 // ScrollUp scrolls the view up n lines (if possible)
117 func (v *View) ScrollUp(n int) {
118         // Try to scroll by n but if it would overflow, scroll by 1
119         if v.Topline-n >= 0 {
120                 v.Topline -= n
121         } else if v.Topline > 0 {
122                 v.Topline--
123         }
124 }
125
126 // ScrollDown scrolls the view down n lines (if possible)
127 func (v *View) ScrollDown(n int) {
128         // Try to scroll by n but if it would overflow, scroll by 1
129         if v.Topline+n <= v.Buf.NumLines-v.height {
130                 v.Topline += n
131         } else if v.Topline < v.Buf.NumLines-v.height {
132                 v.Topline++
133         }
134 }
135
136 // CanClose returns whether or not the view can be closed
137 // If there are unsaved changes, the user will be asked if the view can be closed
138 // causing them to lose the unsaved changes
139 // The message is what to print after saying "You have unsaved changes. "
140 func (v *View) CanClose(msg string) bool {
141         if v.Buf.IsModified {
142                 quit, canceled := messenger.Prompt("You have unsaved changes. " + msg)
143                 if !canceled {
144                         if strings.ToLower(quit) == "yes" || strings.ToLower(quit) == "y" {
145                                 return true
146                         } else if strings.ToLower(quit) == "save" || strings.ToLower(quit) == "s" {
147                                 v.Save()
148                                 return true
149                         }
150                 }
151         } else {
152                 return true
153         }
154         return false
155 }
156
157 // OpenBuffer opens a new buffer in this view.
158 // This resets the topline, event handler and cursor.
159 func (v *View) OpenBuffer(buf *Buffer) {
160         v.Buf = buf
161         v.Cursor = &buf.Cursor
162         v.Topline = 0
163         v.leftCol = 0
164         v.Cursor.ResetSelection()
165         v.messages = make(map[string][]GutterMessage)
166
167         v.matches = Match(v)
168
169         // Set mouseReleased to true because we assume the mouse is not being pressed when
170         // the editor is opened
171         v.mouseReleased = true
172         v.lastClickTime = time.Time{}
173 }
174
175 // ReOpen reloads the current buffer
176 func (v *View) ReOpen() {
177         if v.CanClose("Continue? (yes, no, save) ") {
178                 file, err := ioutil.ReadFile(v.Buf.Path)
179                 filename := v.Buf.Name
180
181                 if err != nil {
182                         messenger.Error(err.Error())
183                         return
184                 }
185                 buf := NewBuffer(string(file), filename)
186                 v.Buf = buf
187                 v.matches = Match(v)
188                 v.Cursor.Relocate()
189                 v.Relocate()
190         }
191 }
192
193 // Relocate moves the view window so that the cursor is in view
194 // This is useful if the user has scrolled far away, and then starts typing
195 func (v *View) Relocate() bool {
196         ret := false
197         cy := v.Cursor.y
198         scrollmargin := int(settings["scrollmargin"].(float64))
199         if cy < v.Topline+scrollmargin && cy > scrollmargin-1 {
200                 v.Topline = cy - scrollmargin
201                 ret = true
202         } else if cy < v.Topline {
203                 v.Topline = cy
204                 ret = true
205         }
206         if cy > v.Topline+v.height-1-scrollmargin {
207                 v.Topline = cy - v.height + 1 + scrollmargin
208                 ret = true
209         }
210
211         cx := v.Cursor.GetVisualX()
212         if cx < v.leftCol {
213                 v.leftCol = cx
214                 ret = true
215         }
216         if cx+v.lineNumOffset+1 > v.leftCol+v.width {
217                 v.leftCol = cx - v.width + v.lineNumOffset + 1
218                 ret = true
219         }
220         return ret
221 }
222
223 // MoveToMouseClick moves the cursor to location x, y assuming x, y were given
224 // by a mouse click
225 func (v *View) MoveToMouseClick(x, y int) {
226         if y-v.Topline > v.height-1 {
227                 v.ScrollDown(1)
228                 y = v.height + v.Topline - 1
229         }
230         if y >= v.Buf.NumLines {
231                 y = v.Buf.NumLines - 1
232         }
233         if y < 0 {
234                 y = 0
235         }
236         if x < 0 {
237                 x = 0
238         }
239
240         x = v.Cursor.GetCharPosInLine(y, x)
241         if x > Count(v.Buf.Lines[y]) {
242                 x = Count(v.Buf.Lines[y])
243         }
244         v.Cursor.x = x
245         v.Cursor.y = y
246         v.Cursor.lastVisualX = v.Cursor.GetVisualX()
247 }
248
249 // HandleEvent handles an event passed by the main loop
250 func (v *View) HandleEvent(event tcell.Event) {
251         // This bool determines whether the view is relocated at the end of the function
252         // By default it's true because most events should cause a relocate
253         relocate := true
254
255         switch e := event.(type) {
256         case *tcell.EventResize:
257                 // Window resized
258                 v.Resize(e.Size())
259         case *tcell.EventKey:
260                 if e.Key() == tcell.KeyRune && e.Modifiers() == 0 {
261                         // Insert a character
262                         if v.Cursor.HasSelection() {
263                                 v.Cursor.DeleteSelection()
264                                 v.Cursor.ResetSelection()
265                         }
266                         v.Buf.Insert(v.Cursor.Loc(), string(e.Rune()))
267                         v.Cursor.Right()
268                 } else {
269                         for key, action := range bindings {
270                                 if e.Key() == key.keyCode {
271                                         if e.Key() == tcell.KeyRune {
272                                                 if e.Rune() != key.r {
273                                                         continue
274                                                 }
275                                         }
276                                         if e.Modifiers() == key.modifiers {
277                                                 relocate = action(v)
278                                                 for _, pl := range loadedPlugins {
279                                                         funcName := strings.Split(runtime.FuncForPC(reflect.ValueOf(action).Pointer()).Name(), ".")
280                                                         err := Call(pl + "_on" + funcName[len(funcName)-1])
281                                                         if err != nil {
282                                                                 TermMessage(err)
283                                                         }
284                                                 }
285                                         }
286                                 }
287                         }
288                 }
289         case *tcell.EventPaste:
290                 if v.Cursor.HasSelection() {
291                         v.Cursor.DeleteSelection()
292                         v.Cursor.ResetSelection()
293                 }
294                 clip := e.Text()
295                 v.Buf.Insert(v.Cursor.Loc(), clip)
296                 v.Cursor.SetLoc(v.Cursor.Loc() + Count(clip))
297                 v.freshClip = false
298         case *tcell.EventMouse:
299                 x, y := e.Position()
300                 x -= v.lineNumOffset - v.leftCol
301                 y += v.Topline
302                 // Don't relocate for mouse events
303                 relocate = false
304
305                 button := e.Buttons()
306
307                 switch button {
308                 case tcell.Button1:
309                         // Left click
310                         if v.mouseReleased {
311                                 v.MoveToMouseClick(x, y)
312                                 if time.Since(v.lastClickTime)/time.Millisecond < doubleClickThreshold {
313                                         if v.doubleClick {
314                                                 // Triple click
315                                                 v.lastClickTime = time.Now()
316
317                                                 v.tripleClick = true
318                                                 v.doubleClick = false
319
320                                                 v.Cursor.SelectLine()
321                                         } else {
322                                                 // Double click
323                                                 v.lastClickTime = time.Now()
324
325                                                 v.doubleClick = true
326                                                 v.tripleClick = false
327
328                                                 v.Cursor.SelectWord()
329                                         }
330                                 } else {
331                                         v.doubleClick = false
332                                         v.tripleClick = false
333                                         v.lastClickTime = time.Now()
334
335                                         loc := v.Cursor.Loc()
336                                         v.Cursor.origSelection[0] = loc
337                                         v.Cursor.curSelection[0] = loc
338                                         v.Cursor.curSelection[1] = loc
339                                 }
340                                 v.mouseReleased = false
341                         } else if !v.mouseReleased {
342                                 v.MoveToMouseClick(x, y)
343                                 if v.tripleClick {
344                                         v.Cursor.AddLineToSelection()
345                                 } else if v.doubleClick {
346                                         v.Cursor.AddWordToSelection()
347                                 } else {
348                                         v.Cursor.curSelection[1] = v.Cursor.Loc()
349                                 }
350                         }
351                 case tcell.ButtonNone:
352                         // Mouse event with no click
353                         if !v.mouseReleased {
354                                 // Mouse was just released
355
356                                 // Relocating here isn't really necessary because the cursor will
357                                 // be in the right place from the last mouse event
358                                 // However, if we are running in a terminal that doesn't support mouse motion
359                                 // events, this still allows the user to make selections, except only after they
360                                 // release the mouse
361
362                                 if !v.doubleClick && !v.tripleClick {
363                                         v.MoveToMouseClick(x, y)
364                                         v.Cursor.curSelection[1] = v.Cursor.Loc()
365                                 }
366                                 v.mouseReleased = true
367                         }
368                 case tcell.WheelUp:
369                         // Scroll up
370                         scrollspeed := int(settings["scrollspeed"].(float64))
371                         v.ScrollUp(scrollspeed)
372                 case tcell.WheelDown:
373                         // Scroll down
374                         scrollspeed := int(settings["scrollspeed"].(float64))
375                         v.ScrollDown(scrollspeed)
376                 }
377         }
378
379         if relocate {
380                 v.Relocate()
381         }
382         if settings["syntax"].(bool) {
383                 v.matches = Match(v)
384         }
385 }
386
387 // GutterMessage creates a message in this view's gutter
388 func (v *View) GutterMessage(section string, lineN int, msg string, kind int) {
389         lineN--
390         gutterMsg := GutterMessage{
391                 lineNum: lineN,
392                 msg:     msg,
393                 kind:    kind,
394         }
395         for _, v := range v.messages {
396                 for _, gmsg := range v {
397                         if gmsg.lineNum == lineN {
398                                 return
399                         }
400                 }
401         }
402         messages := v.messages[section]
403         v.messages[section] = append(messages, gutterMsg)
404 }
405
406 // ClearGutterMessages clears all gutter messages from a given section
407 func (v *View) ClearGutterMessages(section string) {
408         v.messages[section] = []GutterMessage{}
409 }
410
411 // ClearAllGutterMessages clears all the gutter messages
412 func (v *View) ClearAllGutterMessages() {
413         for k := range v.messages {
414                 v.messages[k] = []GutterMessage{}
415         }
416 }
417
418 // DisplayView renders the view to the screen
419 func (v *View) DisplayView() {
420         // The character number of the character in the top left of the screen
421         charNum := ToCharPos(0, v.Topline, v.Buf)
422
423         // Convert the length of buffer to a string, and get the length of the string
424         // We are going to have to offset by that amount
425         maxLineLength := len(strconv.Itoa(v.Buf.NumLines))
426         // + 1 for the little space after the line number
427         if settings["ruler"] == true {
428                 v.lineNumOffset = maxLineLength + 1
429         } else {
430                 v.lineNumOffset = 0
431         }
432         var highlightStyle tcell.Style
433
434         var hasGutterMessages bool
435         for _, v := range v.messages {
436                 if len(v) > 0 {
437                         hasGutterMessages = true
438                 }
439         }
440         if hasGutterMessages {
441                 v.lineNumOffset += 2
442         }
443
444         for lineN := 0; lineN < v.height; lineN++ {
445                 var x int
446                 // If the buffer is smaller than the view height
447                 // and we went too far, break
448                 if lineN+v.Topline >= v.Buf.NumLines {
449                         break
450                 }
451                 line := v.Buf.Lines[lineN+v.Topline]
452
453                 if hasGutterMessages {
454                         msgOnLine := false
455                         for k := range v.messages {
456                                 for _, msg := range v.messages[k] {
457                                         if msg.lineNum == lineN+v.Topline {
458                                                 msgOnLine = true
459                                                 gutterStyle := tcell.StyleDefault
460                                                 switch msg.kind {
461                                                 case GutterInfo:
462                                                         if style, ok := colorscheme["gutter-info"]; ok {
463                                                                 gutterStyle = style
464                                                         }
465                                                 case GutterWarning:
466                                                         if style, ok := colorscheme["gutter-warning"]; ok {
467                                                                 gutterStyle = style
468                                                         }
469                                                 case GutterError:
470                                                         if style, ok := colorscheme["gutter-error"]; ok {
471                                                                 gutterStyle = style
472                                                         }
473                                                 }
474                                                 screen.SetContent(x, lineN, '>', nil, gutterStyle)
475                                                 x++
476                                                 screen.SetContent(x, lineN, '>', nil, gutterStyle)
477                                                 x++
478                                                 if v.Cursor.y == lineN+v.Topline {
479                                                         messenger.Message(msg.msg)
480                                                         messenger.gutterMessage = true
481                                                 }
482                                         }
483                                 }
484                         }
485                         if !msgOnLine {
486                                 screen.SetContent(x, lineN, ' ', nil, tcell.StyleDefault)
487                                 x++
488                                 screen.SetContent(x, lineN, ' ', nil, tcell.StyleDefault)
489                                 x++
490                                 if v.Cursor.y == lineN+v.Topline && messenger.gutterMessage {
491                                         messenger.Reset()
492                                         messenger.gutterMessage = false
493                                 }
494                         }
495                 }
496
497                 // Write the line number
498                 lineNumStyle := defStyle
499                 if style, ok := colorscheme["line-number"]; ok {
500                         lineNumStyle = style
501                 }
502                 // Write the spaces before the line number if necessary
503                 var lineNum string
504                 if settings["ruler"] == true {
505                         lineNum = strconv.Itoa(lineN + v.Topline + 1)
506                         for i := 0; i < maxLineLength-len(lineNum); i++ {
507                                 screen.SetContent(x, lineN, ' ', nil, lineNumStyle)
508                                 x++
509                         }
510                         // Write the actual line number
511                         for _, ch := range lineNum {
512                                 screen.SetContent(x, lineN, ch, nil, lineNumStyle)
513                                 x++
514                         }
515
516                         if settings["ruler"] == true {
517                                 // Write the extra space
518                                 screen.SetContent(x, lineN, ' ', nil, lineNumStyle)
519                                 x++
520                         }
521                 }
522                 // Write the line
523                 tabchars := 0
524                 for colN, ch := range line {
525                         var lineStyle tcell.Style
526
527                         if settings["syntax"].(bool) {
528                                 // Syntax highlighting is enabled
529                                 highlightStyle = v.matches[lineN][colN]
530                         }
531
532                         if v.Cursor.HasSelection() &&
533                                 (charNum >= v.Cursor.curSelection[0] && charNum < v.Cursor.curSelection[1] ||
534                                         charNum < v.Cursor.curSelection[0] && charNum >= v.Cursor.curSelection[1]) {
535
536                                 lineStyle = tcell.StyleDefault.Reverse(true)
537
538                                 if style, ok := colorscheme["selection"]; ok {
539                                         lineStyle = style
540                                 }
541                         } else {
542                                 lineStyle = highlightStyle
543                         }
544
545                         if ch == '\t' {
546                                 lineIndentStyle := defStyle
547                                 if style, ok := colorscheme["indent-char"]; ok {
548                                         lineIndentStyle = style
549                                 }
550                                 if v.Cursor.HasSelection() &&
551                                         (charNum >= v.Cursor.curSelection[0] && charNum < v.Cursor.curSelection[1] ||
552                                                 charNum < v.Cursor.curSelection[0] && charNum >= v.Cursor.curSelection[1]) {
553
554                                         lineIndentStyle = tcell.StyleDefault.Reverse(true)
555
556                                         if style, ok := colorscheme["selection"]; ok {
557                                                 lineIndentStyle = style
558                                         }
559                                 }
560                                 indentChar := []rune(settings["indentchar"].(string))
561                                 screen.SetContent(x-v.leftCol+tabchars, lineN, indentChar[0], nil, lineIndentStyle)
562                                 tabSize := int(settings["tabsize"].(float64))
563                                 for i := 0; i < tabSize-1; i++ {
564                                         tabchars++
565                                         if x-v.leftCol+tabchars >= v.lineNumOffset {
566                                                 screen.SetContent(x-v.leftCol+tabchars, lineN, ' ', nil, lineStyle)
567                                         }
568                                 }
569                         } else {
570                                 if x-v.leftCol+tabchars >= v.lineNumOffset {
571                                         screen.SetContent(x-v.leftCol+tabchars, lineN, ch, nil, lineStyle)
572                                 }
573                         }
574                         charNum++
575                         x++
576                 }
577                 // Here we are at a newline
578
579                 // The newline may be selected, in which case we should draw the selection style
580                 // with a space to represent it
581                 if v.Cursor.HasSelection() &&
582                         (charNum >= v.Cursor.curSelection[0] && charNum < v.Cursor.curSelection[1] ||
583                                 charNum < v.Cursor.curSelection[0] && charNum >= v.Cursor.curSelection[1]) {
584
585                         selectStyle := defStyle.Reverse(true)
586
587                         if style, ok := colorscheme["selection"]; ok {
588                                 selectStyle = style
589                         }
590                         screen.SetContent(x-v.leftCol+tabchars, lineN, ' ', nil, selectStyle)
591                 }
592
593                 charNum++
594         }
595 }
596
597 // DisplayCursor draws the current buffer's cursor to the screen
598 func (v *View) DisplayCursor() {
599         // Don't draw the cursor if it is out of the viewport or if it has a selection
600         if (v.Cursor.y-v.Topline < 0 || v.Cursor.y-v.Topline > v.height-1) || v.Cursor.HasSelection() {
601                 screen.HideCursor()
602         } else {
603                 screen.ShowCursor(v.Cursor.GetVisualX()+v.lineNumOffset-v.leftCol, v.Cursor.y-v.Topline)
604         }
605 }
606
607 // Display renders the view, the cursor, and statusline
608 func (v *View) Display() {
609         v.DisplayView()
610         v.DisplayCursor()
611         if settings["statusline"].(bool) {
612                 v.sline.Display()
613         }
614 }