]> git.lizzy.rs Git - micro.git/blob - src/view.go
f9d90e8f7b270d3c8a61a4635eed892ab7aef0ec
[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                 if len(v.buf.lines) >= v.height {
183                         v.topline = len(v.buf.lines) - v.height
184                 }
185         }
186 }
187
188 // CanClose returns whether or not the view can be closed
189 // If there are unsaved changes, the user will be asked if the view can be closed
190 // causing them to lose the unsaved changes
191 // The message is what to print after saying "You have unsaved changes. "
192 func (v *View) CanClose(msg string) bool {
193         if v.buf.IsDirty() {
194                 quit, canceled := messenger.Prompt("You have unsaved changes. " + msg)
195                 if !canceled {
196                         if strings.ToLower(quit) == "yes" || strings.ToLower(quit) == "y" {
197                                 return true
198                         }
199                 }
200         } else {
201                 return true
202         }
203         return false
204 }
205
206 // Save the buffer to disk
207 func (v *View) Save() {
208         // If this is an empty buffer, ask for a filename
209         if v.buf.path == "" {
210                 filename, canceled := messenger.Prompt("Filename: ")
211                 if !canceled {
212                         v.buf.path = filename
213                         v.buf.name = filename
214                 } else {
215                         return
216                 }
217         }
218         err := v.buf.Save()
219         if err != nil {
220                 messenger.Error(err.Error())
221         }
222         messenger.Message("Saved " + v.buf.path)
223 }
224
225 // Copy the selection to the system clipboard
226 func (v *View) Copy() {
227         if v.cursor.HasSelection() {
228                 if !clipboard.Unsupported {
229                         clipboard.WriteAll(v.cursor.GetSelection())
230                 } else {
231                         messenger.Error("Clipboard is not supported on your system")
232                 }
233         }
234 }
235
236 // Cut the selection to the system clipboard
237 func (v *View) Cut() {
238         if v.cursor.HasSelection() {
239                 if !clipboard.Unsupported {
240                         clipboard.WriteAll(v.cursor.GetSelection())
241                         v.cursor.DeleteSelection()
242                         v.cursor.ResetSelection()
243                 } else {
244                         messenger.Error("Clipboard is not supported on your system")
245                 }
246         }
247 }
248
249 // Paste whatever is in the system clipboard into the buffer
250 // Delete and paste if the user has a selection
251 func (v *View) Paste() {
252         if !clipboard.Unsupported {
253                 if v.cursor.HasSelection() {
254                         v.cursor.DeleteSelection()
255                         v.cursor.ResetSelection()
256                 }
257                 clip, _ := clipboard.ReadAll()
258                 v.eh.Insert(v.cursor.Loc(), clip)
259                 // This is a bit weird... Not sure if there's a better way
260                 for i := 0; i < Count(clip); i++ {
261                         v.cursor.Right()
262                 }
263         } else {
264                 messenger.Error("Clipboard is not supported on your system")
265         }
266 }
267
268 // SelectAll selects the entire buffer
269 func (v *View) SelectAll() {
270         v.cursor.curSelection[1] = 0
271         v.cursor.curSelection[0] = v.buf.Len()
272         // Put the cursor at the beginning
273         v.cursor.x = 0
274         v.cursor.y = 0
275 }
276
277 // OpenFile opens a new file in the current view
278 // It makes sure that the current buffer can be closed first (unsaved changes)
279 func (v *View) OpenFile() {
280         if v.CanClose("Continue? ") {
281                 filename, canceled := messenger.Prompt("File to open: ")
282                 if canceled {
283                         return
284                 }
285                 file, err := ioutil.ReadFile(filename)
286
287                 if err != nil {
288                         messenger.Error(err.Error())
289                         return
290                 }
291                 v.buf = NewBuffer(string(file), filename)
292         }
293 }
294
295 // Relocate moves the view window so that the cursor is in view
296 // This is useful if the user has scrolled far away, and then starts typing
297 func (v *View) Relocate() {
298         cy := v.cursor.y
299         if cy < v.topline {
300                 v.topline = cy
301         }
302         if cy > v.topline+v.height-1 {
303                 v.topline = cy - v.height + 1
304         }
305
306         cx := v.cursor.GetVisualX()
307         if cx < v.leftCol {
308                 v.leftCol = cx
309         }
310         if cx+v.lineNumOffset+1 > v.leftCol+v.width {
311                 v.leftCol = cx - v.width + v.lineNumOffset + 1
312         }
313 }
314
315 // MoveToMouseClick moves the cursor to location x, y assuming x, y were given
316 // by a mouse click
317 func (v *View) MoveToMouseClick(x, y int) {
318         if y-v.topline > v.height-1 {
319                 v.ScrollDown(1)
320                 y = v.height + v.topline - 1
321         }
322         if y >= len(v.buf.lines) {
323                 y = len(v.buf.lines) - 1
324         }
325         if x < 0 {
326                 x = 0
327         }
328
329         x = v.cursor.GetCharPosInLine(y, x)
330         if x > Count(v.buf.lines[y]) {
331                 x = Count(v.buf.lines[y])
332         }
333         v.cursor.x = x
334         v.cursor.y = y
335         v.cursor.lastVisualX = v.cursor.GetVisualX()
336 }
337
338 // HandleEvent handles an event passed by the main loop
339 func (v *View) HandleEvent(event tcell.Event) {
340         // This bool determines whether the view is relocated at the end of the function
341         // By default it's true because most events should cause a relocate
342         relocate := true
343         // By default we don't update and syntax highlighting
344         v.UpdateLines(-2, 0)
345         switch e := event.(type) {
346         case *tcell.EventResize:
347                 // Window resized
348                 v.Resize(e.Size())
349         case *tcell.EventKey:
350                 switch e.Key() {
351                 case tcell.KeyUp:
352                         // Cursor up
353                         v.cursor.Up()
354                 case tcell.KeyDown:
355                         // Cursor down
356                         v.cursor.Down()
357                 case tcell.KeyLeft:
358                         // Cursor left
359                         v.cursor.Left()
360                 case tcell.KeyRight:
361                         // Cursor right
362                         v.cursor.Right()
363                 case tcell.KeyEnter:
364                         // Insert a newline
365                         v.eh.Insert(v.cursor.Loc(), "\n")
366                         v.cursor.Right()
367                         // Rehighlight the entire buffer
368                         v.UpdateLines(v.topline, v.topline+v.height)
369                         // v.UpdateLines(v.cursor.y-1, v.cursor.y)
370                 case tcell.KeySpace:
371                         // Insert a space
372                         v.eh.Insert(v.cursor.Loc(), " ")
373                         v.cursor.Right()
374                         v.UpdateLines(v.cursor.y, v.cursor.y)
375                 case tcell.KeyBackspace2:
376                         // Delete a character
377                         if v.cursor.HasSelection() {
378                                 v.cursor.DeleteSelection()
379                                 v.cursor.ResetSelection()
380                                 // Rehighlight the entire buffer
381                                 v.UpdateLines(v.topline, v.topline+v.height)
382                         } else if v.cursor.Loc() > 0 {
383                                 // We have to do something a bit hacky here because we want to
384                                 // delete the line by first moving left and then deleting backwards
385                                 // but the undo redo would place the cursor in the wrong place
386                                 // So instead we move left, save the position, move back, delete
387                                 // and restore the position
388                                 v.cursor.Left()
389                                 cx, cy := v.cursor.x, v.cursor.y
390                                 v.cursor.Right()
391                                 loc := v.cursor.Loc()
392                                 v.eh.Remove(loc-1, loc)
393                                 v.cursor.x, v.cursor.y = cx, cy
394                                 // Rehighlight the entire buffer
395                                 v.UpdateLines(v.topline, v.topline+v.height)
396                                 // v.UpdateLines(v.cursor.y, v.cursor.y+1)
397                         }
398                 case tcell.KeyTab:
399                         // Insert a tab
400                         v.eh.Insert(v.cursor.Loc(), "\t")
401                         v.cursor.Right()
402                         v.UpdateLines(v.cursor.y, v.cursor.y)
403                 case tcell.KeyCtrlS:
404                         v.Save()
405                 case tcell.KeyCtrlZ:
406                         v.eh.Undo()
407                         // Rehighlight the entire buffer
408                         v.UpdateLines(v.topline, v.topline+v.height)
409                 case tcell.KeyCtrlY:
410                         v.eh.Redo()
411                         // Rehighlight the entire buffer
412                         v.UpdateLines(v.topline, v.topline+v.height)
413                 case tcell.KeyCtrlC:
414                         v.Copy()
415                         // Rehighlight the entire buffer
416                         v.UpdateLines(v.topline, v.topline+v.height)
417                 case tcell.KeyCtrlX:
418                         v.Cut()
419                         // Rehighlight the entire buffer
420                         v.UpdateLines(v.topline, v.topline+v.height)
421                 case tcell.KeyCtrlV:
422                         v.Paste()
423                         // Rehighlight the entire buffer
424                         v.UpdateLines(v.topline, v.topline+v.height)
425                 case tcell.KeyCtrlA:
426                         v.SelectAll()
427                 case tcell.KeyCtrlO:
428                         v.OpenFile()
429                         // Rehighlight the entire buffer
430                         v.UpdateLines(v.topline, v.topline+v.height)
431                 case tcell.KeyPgUp:
432                         v.PageUp()
433                         relocate = false
434                 case tcell.KeyPgDn:
435                         v.PageDown()
436                         relocate = false
437                 case tcell.KeyCtrlU:
438                         v.HalfPageUp()
439                         relocate = false
440                 case tcell.KeyCtrlD:
441                         v.HalfPageDown()
442                         relocate = false
443                 case tcell.KeyRune:
444                         // Insert a character
445                         if v.cursor.HasSelection() {
446                                 v.cursor.DeleteSelection()
447                                 v.cursor.ResetSelection()
448                                 // Rehighlight the entire buffer
449                                 v.UpdateLines(v.topline, v.topline+v.height)
450                         } else {
451                                 v.UpdateLines(v.cursor.y, v.cursor.y)
452                         }
453                         v.eh.Insert(v.cursor.Loc(), string(e.Rune()))
454                         v.cursor.Right()
455                 }
456         case *tcell.EventMouse:
457                 x, y := e.Position()
458                 x -= v.lineNumOffset - v.leftCol
459                 y += v.topline
460                 // Position always seems to be off by one
461                 x--
462                 y--
463
464                 button := e.Buttons()
465
466                 switch button {
467                 case tcell.Button1:
468                         // Left click
469                         origX, origY := v.cursor.x, v.cursor.y
470                         v.MoveToMouseClick(x, y)
471
472                         if v.mouseReleased {
473                                 if (time.Since(v.lastClickTime)/time.Millisecond < doubleClickThreshold) &&
474                                         (origX == v.cursor.x && origY == v.cursor.y) {
475                                         if v.doubleClick {
476                                                 // Triple click
477                                                 v.lastClickTime = time.Now()
478
479                                                 v.tripleClick = true
480                                                 v.doubleClick = false
481
482                                                 v.cursor.SelectLine()
483                                         } else {
484                                                 // Double click
485                                                 v.lastClickTime = time.Now()
486
487                                                 v.doubleClick = true
488                                                 v.tripleClick = false
489
490                                                 v.cursor.SelectWord()
491                                         }
492                                 } else {
493                                         v.doubleClick = false
494                                         v.tripleClick = false
495                                         v.lastClickTime = time.Now()
496
497                                         loc := v.cursor.Loc()
498                                         v.cursor.curSelection[0] = loc
499                                         v.cursor.curSelection[1] = loc
500                                 }
501                         } else {
502                                 if v.tripleClick {
503                                         v.cursor.AddLineToSelection()
504                                 } else if v.doubleClick {
505                                         v.cursor.AddWordToSelection()
506                                 } else {
507                                         v.cursor.curSelection[1] = v.cursor.Loc()
508                                 }
509                         }
510                         v.mouseReleased = false
511
512                 case tcell.ButtonNone:
513                         // Mouse event with no click
514                         if !v.mouseReleased {
515                                 // Mouse was just released
516
517                                 // Relocating here isn't really necessary because the cursor will
518                                 // be in the right place from the last mouse event
519                                 // However, if we are running in a terminal that doesn't support mouse motion
520                                 // events, this still allows the user to make selections, except only after they
521                                 // release the mouse
522
523                                 if !v.doubleClick && !v.tripleClick {
524                                         v.MoveToMouseClick(x, y)
525                                         v.cursor.curSelection[1] = v.cursor.Loc()
526                                 }
527                                 v.mouseReleased = true
528                         }
529                         // We don't want to relocate because otherwise the view will be relocated
530                         // every time the user moves the cursor
531                         relocate = false
532                 case tcell.WheelUp:
533                         // Scroll up two lines
534                         v.ScrollUp(2)
535                         // We don't want to relocate if the user is scrolling
536                         relocate = false
537                         // Rehighlight the entire buffer
538                         v.UpdateLines(v.topline, v.topline+v.height)
539                 case tcell.WheelDown:
540                         // Scroll down two lines
541                         v.ScrollDown(2)
542                         // We don't want to relocate if the user is scrolling
543                         relocate = false
544                         // Rehighlight the entire buffer
545                         v.UpdateLines(v.topline, v.topline+v.height)
546                 }
547         }
548
549         if relocate {
550                 v.Relocate()
551         }
552
553         v.matches = Match(v)
554 }
555
556 // DisplayView renders the view to the screen
557 func (v *View) DisplayView() {
558         // matches := make(SyntaxMatches, len(v.buf.lines))
559         //
560         // viewStart := v.topline
561         // viewEnd := v.topline + v.height
562         // if viewEnd > len(v.buf.lines) {
563         //      viewEnd = len(v.buf.lines)
564         // }
565         //
566         // lines := v.buf.lines[viewStart:viewEnd]
567         // for i, line := range lines {
568         //      matches[i] = make([]tcell.Style, len(line))
569         // }
570
571         // The character number of the character in the top left of the screen
572         charNum := ToCharPos(0, v.topline, v.buf)
573
574         // Convert the length of buffer to a string, and get the length of the string
575         // We are going to have to offset by that amount
576         maxLineLength := len(strconv.Itoa(len(v.buf.lines)))
577         // + 1 for the little space after the line number
578         v.lineNumOffset = maxLineLength + 1
579
580         var highlightStyle tcell.Style
581
582         for lineN := 0; lineN < v.height; lineN++ {
583                 var x int
584                 // If the buffer is smaller than the view height
585                 // and we went too far, break
586                 if lineN+v.topline >= len(v.buf.lines) {
587                         break
588                 }
589                 line := v.buf.lines[lineN+v.topline]
590
591                 // Write the line number
592                 lineNumStyle := tcell.StyleDefault
593                 if style, ok := colorscheme["line-number"]; ok {
594                         lineNumStyle = style
595                 }
596                 // Write the spaces before the line number if necessary
597                 lineNum := strconv.Itoa(lineN + v.topline + 1)
598                 for i := 0; i < maxLineLength-len(lineNum); i++ {
599                         screen.SetContent(x, lineN, ' ', nil, lineNumStyle)
600                         x++
601                 }
602                 // Write the actual line number
603                 for _, ch := range lineNum {
604                         screen.SetContent(x, lineN, ch, nil, lineNumStyle)
605                         x++
606                 }
607                 // Write the extra space
608                 screen.SetContent(x, lineN, ' ', nil, lineNumStyle)
609                 x++
610
611                 // Write the line
612                 tabchars := 0
613                 for colN, ch := range line {
614                         var lineStyle tcell.Style
615                         // Does the current character need to be syntax highlighted?
616
617                         // if lineN >= v.updateLines[0] && lineN < v.updateLines[1] {
618                         highlightStyle = v.matches[lineN][colN]
619                         // } else if lineN < len(v.lastMatches) && colN < len(v.lastMatches[lineN]) {
620                         // highlightStyle = v.lastMatches[lineN][colN]
621                         // } else {
622                         // highlightStyle = tcell.StyleDefault
623                         // }
624
625                         if v.cursor.HasSelection() &&
626                                 (charNum >= v.cursor.curSelection[0] && charNum <= v.cursor.curSelection[1] ||
627                                         charNum <= v.cursor.curSelection[0] && charNum >= v.cursor.curSelection[1]) {
628
629                                 lineStyle = tcell.StyleDefault.Reverse(true)
630
631                                 if style, ok := colorscheme["selection"]; ok {
632                                         lineStyle = style
633                                 }
634                         } else {
635                                 lineStyle = highlightStyle
636                         }
637                         // matches[lineN][colN] = highlightStyle
638
639                         if ch == '\t' {
640                                 screen.SetContent(x+tabchars, lineN, ' ', nil, lineStyle)
641                                 tabSize := settings.TabSize
642                                 for i := 0; i < tabSize-1; i++ {
643                                         tabchars++
644                                         if x-v.leftCol+tabchars >= v.lineNumOffset {
645                                                 screen.SetContent(x-v.leftCol+tabchars, lineN, ' ', nil, lineStyle)
646                                         }
647                                 }
648                         } else {
649                                 if x-v.leftCol+tabchars >= v.lineNumOffset {
650                                         screen.SetContent(x-v.leftCol+tabchars, lineN, ch, nil, lineStyle)
651                                 }
652                         }
653                         charNum++
654                         x++
655                 }
656                 // Here we are at a newline
657
658                 // The newline may be selected, in which case we should draw the selection style
659                 // with a space to represent it
660                 if v.cursor.HasSelection() &&
661                         (charNum >= v.cursor.curSelection[0] && charNum <= v.cursor.curSelection[1] ||
662                                 charNum <= v.cursor.curSelection[0] && charNum >= v.cursor.curSelection[1]) {
663
664                         selectStyle := tcell.StyleDefault.Reverse(true)
665
666                         if style, ok := colorscheme["selection"]; ok {
667                                 selectStyle = style
668                         }
669                         screen.SetContent(x-v.leftCol+tabchars, lineN, ' ', nil, selectStyle)
670                 }
671
672                 charNum++
673         }
674         // v.lastMatches = matches
675 }
676
677 // Display renders the view, the cursor, and statusline
678 func (v *View) Display() {
679         v.DisplayView()
680         v.cursor.Display()
681         v.sline.Display()
682 }