]> git.lizzy.rs Git - micro.git/blob - src/view.go
Add automatic settings to ~/.micro/settings.json
[micro.git] / src / view.go
1 package main
2
3 import (
4         "github.com/atotto/clipboard"
5         "github.com/gdamore/tcell"
6         "io/ioutil"
7         "strconv"
8         "strings"
9         "time"
10 )
11
12 // The View struct stores information about a view into a buffer.
13 // It has a value for the cursor, and the window that the user sees
14 // the buffer from.
15 type View struct {
16         cursor Cursor
17
18         // The topmost line, used for vertical scrolling
19         topline int
20         // The leftmost column, used for horizontal scrolling
21         leftCol int
22
23         // Percentage of the terminal window that this view takes up (from 0 to 100)
24         widthPercent  int
25         heightPercent int
26
27         // Actual with and height
28         width  int
29         height int
30
31         // How much to offset because of line numbers
32         lineNumOffset int
33
34         // The eventhandler for undo/redo
35         eh *EventHandler
36
37         // The buffer
38         buf *Buffer
39         // The statusline
40         sline Statusline
41
42         // Since tcell doesn't differentiate between a mouse release event
43         // and a mouse move event with no keys pressed, we need to keep
44         // track of whether or not the mouse was pressed (or not released) last event to determine
45         // mouse release events
46         mouseReleased bool
47
48         // This stores when the last click was
49         // This is useful for detecting double and triple clicks
50         lastClickTime time.Time
51
52         // Was the last mouse event actually a double click?
53         // Useful for detecting triple clicks -- if a double click is detected
54         // but the last mouse event was actually a double click, it's a triple click
55         doubleClick bool
56         // Same here, just to keep track for mouse move events
57         tripleClick bool
58
59         // Syntax highlighting matches
60         matches SyntaxMatches
61         // The matches from the last frame
62         lastMatches SyntaxMatches
63
64         // This is the range of lines that should have their syntax highlighting updated
65         updateLines [2]int
66 }
67
68 // NewView returns a new fullscreen view
69 func NewView(buf *Buffer) *View {
70         return NewViewWidthHeight(buf, 100, 100)
71 }
72
73 // NewViewWidthHeight returns a new view with the specified width and height percentages
74 // Note that w and h are percentages not actual values
75 func NewViewWidthHeight(buf *Buffer, w, h int) *View {
76         v := new(View)
77
78         v.buf = buf
79
80         v.widthPercent = w
81         v.heightPercent = h
82         v.Resize(screen.Size())
83
84         v.topline = 0
85         // Put the cursor at the first spot
86         v.cursor = Cursor{
87                 x: 0,
88                 y: 0,
89                 v: v,
90         }
91
92         v.eh = NewEventHandler(v)
93
94         v.sline = Statusline{
95                 view: v,
96         }
97
98         // Update the syntax highlighting for the entire buffer at the start
99         v.UpdateLines(v.topline, v.topline+v.height)
100         v.matches = Match(v)
101
102         // Set mouseReleased to true because we assume the mouse is not being pressed when
103         // the editor is opened
104         v.mouseReleased = true
105         v.lastClickTime = time.Time{}
106
107         return v
108 }
109
110 // UpdateLines sets the values for v.updateLines
111 func (v *View) UpdateLines(start, end int) {
112         v.updateLines[0] = start
113         v.updateLines[1] = end + 1
114 }
115
116 // Resize recalculates the actual width and height of the view from the width and height
117 // percentages
118 // This is usually called when the window is resized, or when a split has been added and
119 // the percentages have changed
120 func (v *View) Resize(w, h int) {
121         // Always include 1 line for the command line at the bottom
122         h--
123         v.width = int(float32(w) * float32(v.widthPercent) / 100)
124         // We subtract 1 for the statusline
125         v.height = int(float32(h)*float32(v.heightPercent)/100) - 1
126 }
127
128 // ScrollUp scrolls the view up n lines (if possible)
129 func (v *View) ScrollUp(n int) {
130         // Try to scroll by n but if it would overflow, scroll by 1
131         if v.topline-n >= 0 {
132                 v.topline -= n
133         } else if v.topline > 0 {
134                 v.topline--
135         }
136 }
137
138 // ScrollDown scrolls the view down n lines (if possible)
139 func (v *View) ScrollDown(n int) {
140         // Try to scroll by n but if it would overflow, scroll by 1
141         if v.topline+n <= len(v.buf.lines)-v.height {
142                 v.topline += n
143         } else if v.topline < len(v.buf.lines)-v.height {
144                 v.topline++
145         }
146 }
147
148 // PageUp scrolls the view up a page
149 func (v *View) PageUp() {
150         if v.topline > v.height {
151                 v.ScrollUp(v.height)
152         } else {
153                 v.topline = 0
154         }
155 }
156
157 // PageDown scrolls the view down a page
158 func (v *View) PageDown() {
159         if len(v.buf.lines)-(v.topline+v.height) > v.height {
160                 v.ScrollDown(v.height)
161         } else {
162                 if len(v.buf.lines) >= v.height {
163                         v.topline = len(v.buf.lines) - v.height
164                 }
165         }
166 }
167
168 // HalfPageUp scrolls the view up half a page
169 func (v *View) HalfPageUp() {
170         if v.topline > v.height/2 {
171                 v.ScrollUp(v.height / 2)
172         } else {
173                 v.topline = 0
174         }
175 }
176
177 // HalfPageDown scrolls the view down half a page
178 func (v *View) HalfPageDown() {
179         if len(v.buf.lines)-(v.topline+v.height) > v.height/2 {
180                 v.ScrollDown(v.height / 2)
181         } else {
182                 v.topline = len(v.buf.lines) - v.height
183         }
184 }
185
186 // CanClose returns whether or not the view can be closed
187 // If there are unsaved changes, the user will be asked if the view can be closed
188 // causing them to lose the unsaved changes
189 // The message is what to print after saying "You have unsaved changes. "
190 func (v *View) CanClose(msg string) bool {
191         if v.buf.IsDirty() {
192                 quit, canceled := messenger.Prompt("You have unsaved changes. " + msg)
193                 if !canceled {
194                         if strings.ToLower(quit) == "yes" || strings.ToLower(quit) == "y" {
195                                 return true
196                         }
197                 }
198         } else {
199                 return true
200         }
201         return false
202 }
203
204 // Save the buffer to disk
205 func (v *View) Save() {
206         // If this is an empty buffer, ask for a filename
207         if v.buf.path == "" {
208                 filename, canceled := messenger.Prompt("Filename: ")
209                 if !canceled {
210                         v.buf.path = filename
211                         v.buf.name = filename
212                 } else {
213                         return
214                 }
215         }
216         err := v.buf.Save()
217         if err != nil {
218                 messenger.Error(err.Error())
219         }
220 }
221
222 // Copy the selection to the system clipboard
223 func (v *View) Copy() {
224         if v.cursor.HasSelection() {
225                 if !clipboard.Unsupported {
226                         clipboard.WriteAll(v.cursor.GetSelection())
227                 } else {
228                         messenger.Error("Clipboard is not supported on your system")
229                 }
230         }
231 }
232
233 // Cut the selection to the system clipboard
234 func (v *View) Cut() {
235         if v.cursor.HasSelection() {
236                 if !clipboard.Unsupported {
237                         clipboard.WriteAll(v.cursor.GetSelection())
238                         v.cursor.DeleteSelection()
239                         v.cursor.ResetSelection()
240                 } else {
241                         messenger.Error("Clipboard is not supported on your system")
242                 }
243         }
244 }
245
246 // Paste whatever is in the system clipboard into the buffer
247 // Delete and paste if the user has a selection
248 func (v *View) Paste() {
249         if !clipboard.Unsupported {
250                 if v.cursor.HasSelection() {
251                         v.cursor.DeleteSelection()
252                         v.cursor.ResetSelection()
253                 }
254                 clip, _ := clipboard.ReadAll()
255                 v.eh.Insert(v.cursor.Loc(), clip)
256                 // This is a bit weird... Not sure if there's a better way
257                 for i := 0; i < Count(clip); i++ {
258                         v.cursor.Right()
259                 }
260         } else {
261                 messenger.Error("Clipboard is not supported on your system")
262         }
263 }
264
265 // SelectAll selects the entire buffer
266 func (v *View) SelectAll() {
267         v.cursor.curSelection[1] = 0
268         v.cursor.curSelection[0] = v.buf.Len()
269         // Put the cursor at the beginning
270         v.cursor.x = 0
271         v.cursor.y = 0
272 }
273
274 // OpenFile opens a new file in the current view
275 // It makes sure that the current buffer can be closed first (unsaved changes)
276 func (v *View) OpenFile() {
277         if v.CanClose("Continue? ") {
278                 filename, canceled := messenger.Prompt("File to open: ")
279                 if canceled {
280                         return
281                 }
282                 file, err := ioutil.ReadFile(filename)
283
284                 if err != nil {
285                         messenger.Error(err.Error())
286                         return
287                 }
288                 v.buf = NewBuffer(string(file), filename)
289         }
290 }
291
292 // Relocate moves the view window so that the cursor is in view
293 // This is useful if the user has scrolled far away, and then starts typing
294 func (v *View) Relocate() {
295         cy := v.cursor.y
296         if cy < v.topline {
297                 v.topline = cy
298         }
299         if cy > v.topline+v.height-1 {
300                 v.topline = cy - v.height + 1
301         }
302 }
303
304 // MoveToMouseClick moves the cursor to location x, y assuming x, y were given
305 // by a mouse click
306 func (v *View) MoveToMouseClick(x, y int) {
307         if y-v.topline > v.height-1 {
308                 v.ScrollDown(1)
309                 y = v.height + v.topline - 1
310         }
311         if y >= len(v.buf.lines) {
312                 y = len(v.buf.lines) - 1
313         }
314         if x < 0 {
315                 x = 0
316         }
317
318         x = v.cursor.GetCharPosInLine(y, x)
319         if x > Count(v.buf.lines[y]) {
320                 x = Count(v.buf.lines[y])
321         }
322         v.cursor.x = x
323         v.cursor.y = y
324 }
325
326 // HandleEvent handles an event passed by the main loop
327 func (v *View) HandleEvent(event tcell.Event) {
328         // This bool determines whether the view is relocated at the end of the function
329         // By default it's true because most events should cause a relocate
330         relocate := true
331         // By default we don't update and syntax highlighting
332         v.UpdateLines(-2, 0)
333         switch e := event.(type) {
334         case *tcell.EventResize:
335                 // Window resized
336                 v.Resize(e.Size())
337         case *tcell.EventKey:
338                 switch e.Key() {
339                 case tcell.KeyUp:
340                         // Cursor up
341                         v.cursor.Up()
342                 case tcell.KeyDown:
343                         // Cursor down
344                         v.cursor.Down()
345                 case tcell.KeyLeft:
346                         // Cursor left
347                         v.cursor.Left()
348                 case tcell.KeyRight:
349                         // Cursor right
350                         v.cursor.Right()
351                 case tcell.KeyEnter:
352                         // Insert a newline
353                         v.eh.Insert(v.cursor.Loc(), "\n")
354                         v.cursor.Right()
355                         // Rehighlight the entire buffer
356                         v.UpdateLines(v.topline, v.topline+v.height)
357                         // v.UpdateLines(v.cursor.y-1, v.cursor.y)
358                 case tcell.KeySpace:
359                         // Insert a space
360                         v.eh.Insert(v.cursor.Loc(), " ")
361                         v.cursor.Right()
362                         v.UpdateLines(v.cursor.y, v.cursor.y)
363                 case tcell.KeyBackspace2:
364                         // Delete a character
365                         if v.cursor.HasSelection() {
366                                 v.cursor.DeleteSelection()
367                                 v.cursor.ResetSelection()
368                                 // Rehighlight the entire buffer
369                                 v.UpdateLines(v.topline, v.topline+v.height)
370                         } else if v.cursor.Loc() > 0 {
371                                 // We have to do something a bit hacky here because we want to
372                                 // delete the line by first moving left and then deleting backwards
373                                 // but the undo redo would place the cursor in the wrong place
374                                 // So instead we move left, save the position, move back, delete
375                                 // and restore the position
376                                 v.cursor.Left()
377                                 cx, cy := v.cursor.x, v.cursor.y
378                                 v.cursor.Right()
379                                 loc := v.cursor.Loc()
380                                 v.eh.Remove(loc-1, loc)
381                                 v.cursor.x, v.cursor.y = cx, cy
382                                 // Rehighlight the entire buffer
383                                 v.UpdateLines(v.topline, v.topline+v.height)
384                                 // v.UpdateLines(v.cursor.y, v.cursor.y+1)
385                         }
386                 case tcell.KeyTab:
387                         // Insert a tab
388                         v.eh.Insert(v.cursor.Loc(), "\t")
389                         v.cursor.Right()
390                         v.UpdateLines(v.cursor.y, v.cursor.y)
391                 case tcell.KeyCtrlS:
392                         v.Save()
393                 case tcell.KeyCtrlZ:
394                         v.eh.Undo()
395                         // Rehighlight the entire buffer
396                         v.UpdateLines(v.topline, v.topline+v.height)
397                 case tcell.KeyCtrlY:
398                         v.eh.Redo()
399                         // Rehighlight the entire buffer
400                         v.UpdateLines(v.topline, v.topline+v.height)
401                 case tcell.KeyCtrlC:
402                         v.Copy()
403                         // Rehighlight the entire buffer
404                         v.UpdateLines(v.topline, v.topline+v.height)
405                 case tcell.KeyCtrlX:
406                         v.Cut()
407                         // Rehighlight the entire buffer
408                         v.UpdateLines(v.topline, v.topline+v.height)
409                 case tcell.KeyCtrlV:
410                         v.Paste()
411                         // Rehighlight the entire buffer
412                         v.UpdateLines(v.topline, v.topline+v.height)
413                 case tcell.KeyCtrlA:
414                         v.SelectAll()
415                 case tcell.KeyCtrlO:
416                         v.OpenFile()
417                         // Rehighlight the entire buffer
418                         v.UpdateLines(v.topline, v.topline+v.height)
419                 case tcell.KeyPgUp:
420                         v.PageUp()
421                         relocate = false
422                 case tcell.KeyPgDn:
423                         v.PageDown()
424                         relocate = false
425                 case tcell.KeyCtrlU:
426                         v.HalfPageUp()
427                         relocate = false
428                 case tcell.KeyCtrlD:
429                         v.HalfPageDown()
430                         relocate = false
431                 case tcell.KeyRune:
432                         // Insert a character
433                         if v.cursor.HasSelection() {
434                                 v.cursor.DeleteSelection()
435                                 v.cursor.ResetSelection()
436                                 // Rehighlight the entire buffer
437                                 v.UpdateLines(v.topline, v.topline+v.height)
438                         } else {
439                                 v.UpdateLines(v.cursor.y, v.cursor.y)
440                         }
441                         v.eh.Insert(v.cursor.Loc(), string(e.Rune()))
442                         v.cursor.Right()
443                 }
444         case *tcell.EventMouse:
445                 x, y := e.Position()
446                 x -= v.lineNumOffset
447                 y += v.topline
448                 // Position always seems to be off by one
449                 x--
450                 y--
451
452                 button := e.Buttons()
453
454                 switch button {
455                 case tcell.Button1:
456                         // Left click
457                         origX, origY := v.cursor.x, v.cursor.y
458                         v.MoveToMouseClick(x, y)
459
460                         if v.mouseReleased {
461                                 if (time.Since(v.lastClickTime)/time.Millisecond < doubleClickThreshold) &&
462                                         (origX == v.cursor.x && origY == v.cursor.y) {
463                                         if v.doubleClick {
464                                                 // Triple click
465                                                 v.lastClickTime = time.Now()
466
467                                                 v.tripleClick = true
468                                                 v.doubleClick = false
469
470                                                 v.cursor.SelectLine()
471                                         } else {
472                                                 // Double click
473                                                 v.lastClickTime = time.Now()
474
475                                                 v.doubleClick = true
476                                                 v.tripleClick = false
477
478                                                 v.cursor.SelectWord()
479                                         }
480                                 } else {
481                                         v.doubleClick = false
482                                         v.tripleClick = false
483                                         v.lastClickTime = time.Now()
484
485                                         loc := v.cursor.Loc()
486                                         v.cursor.curSelection[0] = loc
487                                         v.cursor.curSelection[1] = loc
488                                 }
489                         } else {
490                                 if v.tripleClick {
491                                         v.cursor.AddLineToSelection()
492                                 } else if v.doubleClick {
493                                         v.cursor.AddWordToSelection()
494                                 } else {
495                                         v.cursor.curSelection[1] = v.cursor.Loc()
496                                 }
497                         }
498                         v.mouseReleased = false
499
500                 case tcell.ButtonNone:
501                         // Mouse event with no click
502                         if !v.mouseReleased {
503                                 // Mouse was just released
504
505                                 // Relocating here isn't really necessary because the cursor will
506                                 // be in the right place from the last mouse event
507                                 // However, if we are running in a terminal that doesn't support mouse motion
508                                 // events, this still allows the user to make selections, except only after they
509                                 // release the mouse
510
511                                 if !v.doubleClick && !v.tripleClick {
512                                         v.MoveToMouseClick(x, y)
513                                         v.cursor.curSelection[1] = v.cursor.Loc()
514                                 }
515                                 v.mouseReleased = true
516                         }
517                         // We don't want to relocate because otherwise the view will be relocated
518                         // every time the user moves the cursor
519                         relocate = false
520                 case tcell.WheelUp:
521                         // Scroll up two lines
522                         v.ScrollUp(2)
523                         // We don't want to relocate if the user is scrolling
524                         relocate = false
525                         // Rehighlight the entire buffer
526                         v.UpdateLines(v.topline, v.topline+v.height)
527                 case tcell.WheelDown:
528                         // Scroll down two lines
529                         v.ScrollDown(2)
530                         // We don't want to relocate if the user is scrolling
531                         relocate = false
532                         // Rehighlight the entire buffer
533                         v.UpdateLines(v.topline, v.topline+v.height)
534                 }
535         }
536
537         if relocate {
538                 v.Relocate()
539         }
540
541         v.matches = Match(v)
542 }
543
544 // DisplayView renders the view to the screen
545 func (v *View) DisplayView() {
546         // matches := make(SyntaxMatches, len(v.buf.lines))
547         //
548         // viewStart := v.topline
549         // viewEnd := v.topline + v.height
550         // if viewEnd > len(v.buf.lines) {
551         //      viewEnd = len(v.buf.lines)
552         // }
553         //
554         // lines := v.buf.lines[viewStart:viewEnd]
555         // for i, line := range lines {
556         //      matches[i] = make([]tcell.Style, len(line))
557         // }
558
559         // The character number of the character in the top left of the screen
560         charNum := ToCharPos(0, v.topline, v.buf)
561
562         // Convert the length of buffer to a string, and get the length of the string
563         // We are going to have to offset by that amount
564         maxLineLength := len(strconv.Itoa(len(v.buf.lines)))
565         // + 1 for the little space after the line number
566         v.lineNumOffset = maxLineLength + 1
567
568         var highlightStyle tcell.Style
569
570         for lineN := 0; lineN < v.height; lineN++ {
571                 var x int
572                 // If the buffer is smaller than the view height
573                 // and we went too far, break
574                 if lineN+v.topline >= len(v.buf.lines) {
575                         break
576                 }
577                 line := v.buf.lines[lineN+v.topline]
578
579                 // Write the line number
580                 lineNumStyle := tcell.StyleDefault
581                 if style, ok := colorscheme["line-number"]; ok {
582                         lineNumStyle = style
583                 }
584                 // Write the spaces before the line number if necessary
585                 lineNum := strconv.Itoa(lineN + v.topline + 1)
586                 for i := 0; i < maxLineLength-len(lineNum); i++ {
587                         screen.SetContent(x, lineN, ' ', nil, lineNumStyle)
588                         x++
589                 }
590                 // Write the actual line number
591                 for _, ch := range lineNum {
592                         screen.SetContent(x, lineN, ch, nil, lineNumStyle)
593                         x++
594                 }
595                 // Write the extra space
596                 screen.SetContent(x, lineN, ' ', nil, lineNumStyle)
597                 x++
598
599                 // Write the line
600                 tabchars := 0
601                 for colN, ch := range line {
602                         var lineStyle tcell.Style
603                         // Does the current character need to be syntax highlighted?
604
605                         // if lineN >= v.updateLines[0] && lineN < v.updateLines[1] {
606                         highlightStyle = v.matches[lineN][colN]
607                         // } else if lineN < len(v.lastMatches) && colN < len(v.lastMatches[lineN]) {
608                         // highlightStyle = v.lastMatches[lineN][colN]
609                         // } else {
610                         // highlightStyle = tcell.StyleDefault
611                         // }
612
613                         if v.cursor.HasSelection() &&
614                                 (charNum >= v.cursor.curSelection[0] && charNum <= v.cursor.curSelection[1] ||
615                                         charNum <= v.cursor.curSelection[0] && charNum >= v.cursor.curSelection[1]) {
616
617                                 lineStyle = tcell.StyleDefault.Reverse(true)
618
619                                 if style, ok := colorscheme["selection"]; ok {
620                                         lineStyle = style
621                                 }
622                         } else {
623                                 lineStyle = highlightStyle
624                         }
625                         // matches[lineN][colN] = highlightStyle
626
627                         if ch == '\t' {
628                                 screen.SetContent(x+tabchars, lineN, ' ', nil, lineStyle)
629                                 tabSize := settings.TabSize
630                                 for i := 0; i < tabSize-1; i++ {
631                                         tabchars++
632                                         screen.SetContent(x+tabchars, lineN, ' ', nil, lineStyle)
633                                 }
634                         } else {
635                                 screen.SetContent(x+tabchars, lineN, ch, nil, lineStyle)
636                         }
637                         charNum++
638                         x++
639                 }
640                 // Here we are at a newline
641
642                 // The newline may be selected, in which case we should draw the selection style
643                 // with a space to represent it
644                 if v.cursor.HasSelection() &&
645                         (charNum >= v.cursor.curSelection[0] && charNum <= v.cursor.curSelection[1] ||
646                                 charNum <= v.cursor.curSelection[0] && charNum >= v.cursor.curSelection[1]) {
647
648                         selectStyle := tcell.StyleDefault.Reverse(true)
649
650                         if style, ok := colorscheme["selection"]; ok {
651                                 selectStyle = style
652                         }
653                         screen.SetContent(x+tabchars, lineN, ' ', nil, selectStyle)
654                 }
655
656                 charNum++
657         }
658         // v.lastMatches = matches
659 }
660
661 // Display renders the view, the cursor, and statusline
662 func (v *View) Display() {
663         v.DisplayView()
664         v.cursor.Display()
665         v.sline.Display()
666 }