]> git.lizzy.rs Git - micro.git/blob - src/view.go
Merge branch 'keybindings'
[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         "os"
8         "strconv"
9         "strings"
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         topline int
18         // Leftmost column. Used for horizontal scrolling
19         leftCol int
20
21         // Percentage of the terminal window that this view takes up
22         heightPercent float32
23         widthPercent  float32
24         height        int
25         width         int
26
27         // How much to offset because of line numbers
28         lineNumOffset int
29
30         eh *EventHandler
31
32         buf *Buffer
33         sl  Statusline
34
35         mouseReleased bool
36
37         // Syntax highlighting matches
38         matches map[int]tcell.Style
39
40         m *Messenger
41
42         s tcell.Screen
43 }
44
45 // NewView returns a new view with fullscreen width and height
46 func NewView(buf *Buffer, m *Messenger, s tcell.Screen) *View {
47         return NewViewWidthHeight(buf, m, s, 1, 1)
48 }
49
50 // NewViewWidthHeight returns a new view with the specified width and height percentages
51 func NewViewWidthHeight(buf *Buffer, m *Messenger, s tcell.Screen, w, h float32) *View {
52         v := new(View)
53
54         v.buf = buf
55         v.s = s
56         v.m = m
57
58         v.widthPercent = w
59         v.heightPercent = h
60         v.Resize(s.Size())
61
62         v.topline = 0
63         v.cursor = Cursor{
64                 x:   0,
65                 y:   0,
66                 loc: 0,
67                 v:   v,
68         }
69
70         v.eh = NewEventHandler(v)
71
72         v.sl = Statusline{
73                 v: v,
74         }
75
76         return v
77 }
78
79 // Resize recalculates the width and height of the view based on the width and height percentages
80 func (v *View) Resize(w, h int) {
81         h--
82         v.height = int(float32(h)*v.heightPercent) - 1
83         v.width = int(float32(w) * v.widthPercent)
84 }
85
86 // ScrollUp scrolls the view up n lines (if possible)
87 func (v *View) ScrollUp(n int) {
88         // Try to scroll by n but if it would overflow, scroll by 1
89         if v.topline-n >= 0 {
90                 v.topline -= n
91         } else if v.topline > 0 {
92                 v.topline--
93         }
94 }
95
96 // ScrollDown scrolls the view down n lines (if possible)
97 func (v *View) ScrollDown(n int) {
98         // Try to scroll by n but if it would overflow, scroll by 1
99         if v.topline+n <= len(v.buf.lines)-v.height {
100                 v.topline += n
101         } else if v.topline < len(v.buf.lines)-v.height {
102                 v.topline++
103         }
104 }
105
106 // PageUp scrolls the view up a page
107 func (v *View) PageUp() {
108         if v.topline > v.height {
109                 v.ScrollUp(v.height)
110         } else {
111                 v.topline = 0
112         }
113 }
114
115 // PageDown scrolls the view down a page
116 func (v *View) PageDown() {
117         if len(v.buf.lines)-(v.topline+v.height) > v.height {
118                 v.ScrollDown(v.height)
119         } else {
120                 v.topline = len(v.buf.lines) - v.height
121         }
122 }
123
124 // HalfPageUp scrolls the view up half a page
125 func (v *View) HalfPageUp() {
126         if v.topline > v.height/2 {
127                 v.ScrollUp(v.height / 2)
128         } else {
129                 v.topline = 0
130         }
131 }
132
133 // HalfPageDown scrolls the view down half a page
134 func (v *View) HalfPageDown() {
135         if len(v.buf.lines)-(v.topline+v.height) > v.height/2 {
136                 v.ScrollDown(v.height / 2)
137         } else {
138                 v.topline = len(v.buf.lines) - v.height
139         }
140 }
141
142 // HandleEvent handles an event passed by the main loop
143 // It returns an int describing how the screen needs to be redrawn
144 // 0: Screen does not need to be redrawn
145 // 1: Only the cursor/statusline needs to be redrawn
146 // 2: Everything needs to be redrawn
147 func (v *View) HandleEvent(event tcell.Event) int {
148         var ret int
149         switch e := event.(type) {
150         case *tcell.EventResize:
151                 // Window resized
152                 v.Resize(e.Size())
153                 ret = 2
154         case *tcell.EventKey:
155                 switch e.Key() {
156                 case tcell.KeyCtrlQ:
157                         // Quit
158                         if v.buf.IsDirty() {
159                                 quit, canceled := v.m.Prompt("You have unsaved changes. Quit anyway? ")
160                                 if !canceled {
161                                         if strings.ToLower(quit) == "yes" || strings.ToLower(quit) == "y" {
162                                                 v.s.Fini()
163                                                 os.Exit(0)
164                                         } else {
165                                                 return 2
166                                         }
167                                 } else {
168                                         return 2
169                                 }
170                         } else {
171                                 v.s.Fini()
172                                 os.Exit(0)
173                         }
174                 case tcell.KeyUp:
175                         // Cursor up
176                         v.cursor.Up()
177                         ret = 1
178                 case tcell.KeyDown:
179                         // Cursor down
180                         v.cursor.Down()
181                         ret = 1
182                 case tcell.KeyLeft:
183                         // Cursor left
184                         v.cursor.Left()
185                         ret = 1
186                 case tcell.KeyRight:
187                         // Cursor right
188                         v.cursor.Right()
189                         ret = 1
190                 case tcell.KeyEnter:
191                         // Insert a newline
192                         v.eh.Insert(v.cursor.loc, "\n")
193                         v.cursor.Right()
194                         ret = 2
195                 case tcell.KeySpace:
196                         // Insert a space
197                         v.eh.Insert(v.cursor.loc, " ")
198                         v.cursor.Right()
199                         ret = 2
200                 case tcell.KeyBackspace2:
201                         // Delete a character
202                         if v.cursor.HasSelection() {
203                                 v.cursor.DeleteSelection()
204                                 v.cursor.ResetSelection()
205                                 ret = 2
206                         } else if v.cursor.loc > 0 {
207                                 // We have to do something a bit hacky here because we want to
208                                 // delete the line by first moving left and then deleting backwards
209                                 // but the undo redo would place the cursor in the wrong place
210                                 // So instead we move left, save the position, move back, delete
211                                 // and restore the position
212                                 v.cursor.Left()
213                                 cx, cy, cloc := v.cursor.x, v.cursor.y, v.cursor.loc
214                                 v.cursor.Right()
215                                 v.eh.Remove(v.cursor.loc-1, v.cursor.loc)
216                                 v.cursor.x, v.cursor.y, v.cursor.loc = cx, cy, cloc
217                                 ret = 2
218                         }
219                 case tcell.KeyTab:
220                         // Insert a tab
221                         v.eh.Insert(v.cursor.loc, "\t")
222                         v.cursor.Right()
223                         ret = 2
224                 case tcell.KeyCtrlS:
225                         // Save
226                         if v.buf.path == "" {
227                                 filename, canceled := v.m.Prompt("Filename: ")
228                                 if !canceled {
229                                         v.buf.path = filename
230                                         v.buf.name = filename
231                                 } else {
232                                         return 2
233                                 }
234                         }
235                         err := v.buf.Save()
236                         if err != nil {
237                                 v.m.Error(err.Error())
238                         }
239                         // Need to redraw the status line
240                         ret = 1
241                 case tcell.KeyCtrlZ:
242                         // Undo
243                         v.eh.Undo()
244                         ret = 2
245                 case tcell.KeyCtrlY:
246                         // Redo
247                         v.eh.Redo()
248                         ret = 2
249                 case tcell.KeyCtrlC:
250                         // Copy
251                         if v.cursor.HasSelection() {
252                                 if !clipboard.Unsupported {
253                                         clipboard.WriteAll(v.cursor.GetSelection())
254                                         ret = 2
255                                 }
256                         }
257                 case tcell.KeyCtrlX:
258                         // Cut
259                         if v.cursor.HasSelection() {
260                                 if !clipboard.Unsupported {
261                                         clipboard.WriteAll(v.cursor.GetSelection())
262                                         v.cursor.DeleteSelection()
263                                         v.cursor.ResetSelection()
264                                         ret = 2
265                                 }
266                         }
267                 case tcell.KeyCtrlV:
268                         // Paste
269                         if !clipboard.Unsupported {
270                                 if v.cursor.HasSelection() {
271                                         v.cursor.DeleteSelection()
272                                         v.cursor.ResetSelection()
273                                 }
274                                 clip, _ := clipboard.ReadAll()
275                                 v.eh.Insert(v.cursor.loc, clip)
276                                 // This is a bit weird... Not sure if there's a better way
277                                 for i := 0; i < Count(clip); i++ {
278                                         v.cursor.Right()
279                                 }
280                                 ret = 2
281                         }
282                 case tcell.KeyCtrlA:
283                         // Select all
284                         v.cursor.selectionEnd = 0
285                         v.cursor.selectionStart = v.buf.Len()
286                         v.cursor.x = 0
287                         v.cursor.y = 0
288                         v.cursor.loc = 0
289                         ret = 2
290                 case tcell.KeyCtrlO:
291                         // Open file
292                         if v.buf.IsDirty() {
293                                 quit, canceled := v.m.Prompt("You have unsaved changes. Continue? ")
294                                 if !canceled {
295                                         if strings.ToLower(quit) == "yes" || strings.ToLower(quit) == "y" {
296                                                 return v.OpenFile()
297                                         } else {
298                                                 return 2
299                                         }
300                                 } else {
301                                         return 2
302                                 }
303                         } else {
304                                 return v.OpenFile()
305                         }
306                 case tcell.KeyPgUp:
307                         // Page up
308                         v.PageUp()
309                         return 2
310                 case tcell.KeyPgDn:
311                         // Page down
312                         v.PageDown()
313                         return 2
314                 case tcell.KeyCtrlU:
315                         // Half page up
316                         v.HalfPageUp()
317                         return 2
318                 case tcell.KeyCtrlD:
319                         // Half page down
320                         v.HalfPageDown()
321                         return 2
322                 case tcell.KeyRune:
323                         // Insert a character
324                         if v.cursor.HasSelection() {
325                                 v.cursor.DeleteSelection()
326                                 v.cursor.ResetSelection()
327                         }
328                         v.eh.Insert(v.cursor.loc, string(e.Rune()))
329                         v.cursor.Right()
330                         ret = 2
331                 }
332         case *tcell.EventMouse:
333                 x, y := e.Position()
334                 x -= v.lineNumOffset
335                 y += v.topline
336                 // Position always seems to be off by one
337                 x--
338                 y--
339
340                 button := e.Buttons()
341
342                 switch button {
343                 case tcell.Button1:
344                         // Left click
345                         if y-v.topline > v.height-1 {
346                                 v.ScrollDown(1)
347                                 y = v.height + v.topline - 1
348                         }
349                         if y >= len(v.buf.lines) {
350                                 y = len(v.buf.lines) - 1
351                         }
352                         if x < 0 {
353                                 x = 0
354                         }
355
356                         x = v.cursor.GetCharPosInLine(y, x)
357                         if x > Count(v.buf.lines[y]) {
358                                 x = Count(v.buf.lines[y])
359                         }
360                         d := v.cursor.Distance(x, y)
361                         v.cursor.loc += d
362                         v.cursor.x = x
363                         v.cursor.y = y
364
365                         if v.mouseReleased {
366                                 v.cursor.selectionStart = v.cursor.loc
367                                 v.cursor.selectionStartX = v.cursor.x
368                                 v.cursor.selectionStartY = v.cursor.y
369                         }
370                         v.cursor.selectionEnd = v.cursor.loc
371                         v.mouseReleased = false
372                         return 2
373                 case tcell.ButtonNone:
374                         // Mouse event with no click
375                         v.mouseReleased = true
376                         // We need to directly return here because otherwise the view will
377                         // be readjusted to put the cursor in it, but there may be mouse events
378                         // where the cursor is not (and should not be) be involved
379                         return 0
380                 case tcell.WheelUp:
381                         // Scroll up two lines
382                         v.ScrollUp(2)
383                         return 2
384                 case tcell.WheelDown:
385                         // Scroll down two lines
386                         v.ScrollDown(2)
387                         return 2
388                 }
389         }
390
391         // Reset the view so the cursor is in view
392         cy := v.cursor.y
393         if cy < v.topline {
394                 v.topline = cy
395                 ret = 2
396         }
397         if cy > v.topline+v.height-1 {
398                 v.topline = cy - v.height + 1
399                 ret = 2
400         }
401
402         return ret
403 }
404
405 // OpenFile Prompts the user for a filename and opens the file in the current buffer
406 func (v *View) OpenFile() int {
407         filename, canceled := v.m.Prompt("File to open: ")
408         if canceled {
409                 return 2
410         }
411         file, err := ioutil.ReadFile(filename)
412
413         if err != nil {
414                 v.m.Error(err.Error())
415                 return 2
416         }
417         v.buf = NewBuffer(string(file), filename)
418         return 2
419 }
420
421 // Display renders the view to the screen
422 func (v *View) Display() {
423         var x int
424
425         charNum := v.cursor.loc + v.cursor.Distance(0, v.topline)
426
427         // Convert the length of buffer to a string, and get the length of the string
428         // We are going to have to offset by that amount
429         maxLineLength := len(strconv.Itoa(len(v.buf.lines)))
430         // + 1 for the little space after the line number
431         v.lineNumOffset = maxLineLength + 1
432
433         var highlightStyle tcell.Style
434
435         for lineN := 0; lineN < v.height; lineN++ {
436                 if lineN+v.topline >= len(v.buf.lines) {
437                         break
438                 }
439                 line := v.buf.lines[lineN+v.topline]
440
441                 // Write the line number
442                 lineNumStyle := tcell.StyleDefault
443                 if _, ok := colorscheme["line-number"]; ok {
444                         lineNumStyle = colorscheme["line-number"]
445                 }
446                 // Write the spaces before the line number if necessary
447                 lineNum := strconv.Itoa(lineN + v.topline + 1)
448                 for i := 0; i < maxLineLength-len(lineNum); i++ {
449                         v.s.SetContent(x, lineN, ' ', nil, lineNumStyle)
450                         x++
451                 }
452                 // Write the actual line number
453                 for _, ch := range lineNum {
454                         v.s.SetContent(x, lineN, ch, nil, lineNumStyle)
455                         x++
456                 }
457                 // Write the extra space
458                 v.s.SetContent(x, lineN, ' ', nil, lineNumStyle)
459                 x++
460
461                 // Write the line
462                 tabchars := 0
463                 for _, ch := range line {
464                         var lineStyle tcell.Style
465                         st, ok := v.matches[charNum]
466                         if ok {
467                                 highlightStyle = st
468                         } else {
469                                 highlightStyle = tcell.StyleDefault
470                         }
471
472                         if v.cursor.HasSelection() &&
473                                 (charNum >= v.cursor.selectionStart && charNum <= v.cursor.selectionEnd ||
474                                         charNum <= v.cursor.selectionStart && charNum >= v.cursor.selectionEnd) {
475
476                                 lineStyle = tcell.StyleDefault.Reverse(true)
477
478                                 if _, ok := colorscheme["selection"]; ok {
479                                         lineStyle = colorscheme["selection"]
480                                 }
481                         } else {
482                                 lineStyle = highlightStyle
483                         }
484
485                         if ch == '\t' {
486                                 v.s.SetContent(x+tabchars, lineN, ' ', nil, lineStyle)
487                                 for i := 0; i < tabSize-1; i++ {
488                                         tabchars++
489                                         v.s.SetContent(x+tabchars, lineN, ' ', nil, lineStyle)
490                                 }
491                         } else {
492                                 v.s.SetContent(x+tabchars, lineN, ch, nil, lineStyle)
493                         }
494                         charNum++
495                         x++
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                         selectStyle := tcell.StyleDefault.Reverse(true)
502
503                         if _, ok := colorscheme["selection"]; ok {
504                                 selectStyle = colorscheme["selection"]
505                         }
506                         v.s.SetContent(x+tabchars, lineN, ' ', nil, selectStyle)
507                 }
508
509                 x = 0
510                 st, ok := v.matches[charNum]
511                 if ok {
512                         highlightStyle = st
513                 }
514                 charNum++
515         }
516 }