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