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