]> git.lizzy.rs Git - micro.git/blob - cmd/micro/view.go
Merge pull request #112 from onodera-punpun/customizable_scrolling
[micro.git] / cmd / micro / view.go
1 package main
2
3 import (
4         "io/ioutil"
5         "reflect"
6         "runtime"
7         "strconv"
8         "strings"
9         "time"
10
11         "github.com/zyedidia/tcell"
12 )
13
14 // The View struct stores information about a view into a buffer.
15 // It stores information about the cursor, and the viewport
16 // that the user sees the buffer from.
17 type View struct {
18         Cursor Cursor
19
20         // The topmost line, used for vertical scrolling
21         Topline int
22         // The leftmost column, used for horizontal scrolling
23         leftCol int
24
25         // Percentage of the terminal window that this view takes up (from 0 to 100)
26         widthPercent  int
27         heightPercent int
28
29         // Actual with and height
30         width  int
31         height int
32
33         // How much to offset because of line numbers
34         lineNumOffset int
35
36         // The eventhandler for undo/redo
37         eh *EventHandler
38
39         // Holds the list of gutter messages
40         messages map[string][]GutterMessage
41
42         // The buffer
43         Buf *Buffer
44         // The statusline
45         sline Statusline
46
47         // Since tcell doesn't differentiate between a mouse release event
48         // and a mouse move event with no keys pressed, we need to keep
49         // track of whether or not the mouse was pressed (or not released) last event to determine
50         // mouse release events
51         mouseReleased bool
52
53         // This stores when the last click was
54         // This is useful for detecting double and triple clicks
55         lastClickTime time.Time
56
57         // lastCutTime stores when the last ctrl+k was issued.
58         // It is used for clearing the clipboard to replace it with fresh cut lines.
59         lastCutTime time.Time
60
61         // freshClip returns true if the clipboard has never been pasted.
62         freshClip bool
63
64         // Was the last mouse event actually a double click?
65         // Useful for detecting triple clicks -- if a double click is detected
66         // but the last mouse event was actually a double click, it's a triple click
67         doubleClick bool
68         // Same here, just to keep track for mouse move events
69         tripleClick bool
70
71         // Syntax highlighting matches
72         matches SyntaxMatches
73         // The matches from the last frame
74         lastMatches SyntaxMatches
75 }
76
77 // NewView returns a new fullscreen view
78 func NewView(buf *Buffer) *View {
79         return NewViewWidthHeight(buf, 100, 100)
80 }
81
82 // NewViewWidthHeight returns a new view with the specified width and height percentages
83 // Note that w and h are percentages not actual values
84 func NewViewWidthHeight(buf *Buffer, w, h int) *View {
85         v := new(View)
86
87         v.widthPercent = w
88         v.heightPercent = h
89         v.Resize(screen.Size())
90
91         v.OpenBuffer(buf)
92
93         v.eh = NewEventHandler(v)
94
95         v.messages = make(map[string][]GutterMessage)
96
97         v.sline = Statusline{
98                 view: v,
99         }
100
101         return v
102 }
103
104 // Resize recalculates the actual width and height of the view from the width and height
105 // percentages
106 // This is usually called when the window is resized, or when a split has been added and
107 // the percentages have changed
108 func (v *View) Resize(w, h int) {
109         // Always include 1 line for the command line at the bottom
110         h--
111         v.width = int(float32(w) * float32(v.widthPercent) / 100)
112         // We subtract 1 for the statusline
113         v.height = int(float32(h) * float32(v.heightPercent) / 100)
114         if settings["statusline"].(bool) {
115                 // Make room for the status line if it is enabled
116                 v.height--
117         }
118 }
119
120 // ScrollUp scrolls the view up n lines (if possible)
121 func (v *View) ScrollUp(n int) {
122         // Try to scroll by n but if it would overflow, scroll by 1
123         if v.Topline-n >= 0 {
124                 v.Topline -= n
125         } else if v.Topline > 0 {
126                 v.Topline--
127         }
128 }
129
130 // ScrollDown scrolls the view down n lines (if possible)
131 func (v *View) ScrollDown(n int) {
132         // Try to scroll by n but if it would overflow, scroll by 1
133         if v.Topline+n <= v.Buf.NumLines-v.height {
134                 v.Topline += n
135         } else if v.Topline < v.Buf.NumLines-v.height {
136                 v.Topline++
137         }
138 }
139
140 // CanClose returns whether or not the view can be closed
141 // If there are unsaved changes, the user will be asked if the view can be closed
142 // causing them to lose the unsaved changes
143 // The message is what to print after saying "You have unsaved changes. "
144 func (v *View) CanClose(msg string) bool {
145         if v.Buf.IsModified {
146                 quit, canceled := messenger.Prompt("You have unsaved changes. " + msg)
147                 if !canceled {
148                         if strings.ToLower(quit) == "yes" || strings.ToLower(quit) == "y" {
149                                 return true
150                         } else if strings.ToLower(quit) == "save" || strings.ToLower(quit) == "s" {
151                                 v.Save()
152                                 return true
153                         }
154                 }
155         } else {
156                 return true
157         }
158         return false
159 }
160
161 // OpenBuffer opens a new buffer in this view.
162 // This resets the topline, event handler and cursor.
163 func (v *View) OpenBuffer(buf *Buffer) {
164         v.Buf = buf
165         v.Topline = 0
166         v.leftCol = 0
167         // Put the cursor at the first spot
168         v.Cursor = Cursor{
169                 x: 0,
170                 y: 0,
171                 v: v,
172         }
173         v.Cursor.ResetSelection()
174         v.messages = make(map[string][]GutterMessage)
175
176         v.eh = NewEventHandler(v)
177         v.matches = Match(v)
178
179         // Set mouseReleased to true because we assume the mouse is not being pressed when
180         // the editor is opened
181         v.mouseReleased = true
182         v.lastClickTime = time.Time{}
183 }
184
185 // ReOpen reloads the current buffer
186 func (v *View) ReOpen() {
187         if v.CanClose("Continue? (yes, no, save) ") {
188                 file, err := ioutil.ReadFile(v.Buf.Path)
189                 filename := v.Buf.Name
190
191                 if err != nil {
192                         messenger.Error(err.Error())
193                         return
194                 }
195                 buf := NewBuffer(string(file), filename)
196                 v.Buf = buf
197                 v.matches = Match(v)
198                 v.Cursor.Relocate()
199                 v.Relocate()
200         }
201 }
202
203 // Relocate moves the view window so that the cursor is in view
204 // This is useful if the user has scrolled far away, and then starts typing
205 func (v *View) Relocate() bool {
206         ret := false
207         cy := v.Cursor.y
208         if cy < v.Topline {
209                 v.Topline = cy
210                 ret = true
211         }
212         if cy > v.Topline+v.height-1 {
213                 v.Topline = cy - v.height + 1
214                 ret = true
215         }
216
217         cx := v.Cursor.GetVisualX()
218         if cx < v.leftCol {
219                 v.leftCol = cx
220                 ret = true
221         }
222         if cx+v.lineNumOffset+1 > v.leftCol+v.width {
223                 v.leftCol = cx - v.width + v.lineNumOffset + 1
224                 ret = true
225         }
226         return ret
227 }
228
229 // MoveToMouseClick moves the cursor to location x, y assuming x, y were given
230 // by a mouse click
231 func (v *View) MoveToMouseClick(x, y int) {
232         if y-v.Topline > v.height-1 {
233                 v.ScrollDown(1)
234                 y = v.height + v.Topline - 1
235         }
236         if y >= v.Buf.NumLines {
237                 y = v.Buf.NumLines - 1
238         }
239         if y < 0 {
240                 y = 0
241         }
242         if x < 0 {
243                 x = 0
244         }
245
246         x = v.Cursor.GetCharPosInLine(y, x)
247         if x > Count(v.Buf.Lines[y]) {
248                 x = Count(v.Buf.Lines[y])
249         }
250         v.Cursor.x = x
251         v.Cursor.y = y
252         v.Cursor.lastVisualX = v.Cursor.GetVisualX()
253 }
254
255 // HandleEvent handles an event passed by the main loop
256 func (v *View) HandleEvent(event tcell.Event) {
257         // This bool determines whether the view is relocated at the end of the function
258         // By default it's true because most events should cause a relocate
259         relocate := true
260
261         switch e := event.(type) {
262         case *tcell.EventResize:
263                 // Window resized
264                 v.Resize(e.Size())
265         case *tcell.EventKey:
266                 if e.Key() == tcell.KeyRune {
267                         // Insert a character
268                         if v.Cursor.HasSelection() {
269                                 v.Cursor.DeleteSelection()
270                                 v.Cursor.ResetSelection()
271                         }
272                         v.eh.Insert(v.Cursor.Loc(), string(e.Rune()))
273                         v.Cursor.Right()
274                 } else {
275                         for key, action := range bindings {
276                                 if e.Key() == key {
277                                         relocate = action(v)
278                                         for _, pl := range loadedPlugins {
279                                                 funcName := strings.Split(runtime.FuncForPC(reflect.ValueOf(action).Pointer()).Name(), ".")
280                                                 err := Call(pl + "_on" + funcName[len(funcName)-1])
281                                                 if err != nil {
282                                                         TermMessage(err)
283                                                 }
284                                         }
285                                 }
286                         }
287                 }
288         case *tcell.EventPaste:
289                 if v.Cursor.HasSelection() {
290                         v.Cursor.DeleteSelection()
291                         v.Cursor.ResetSelection()
292                 }
293                 clip := e.Text()
294                 v.eh.Insert(v.Cursor.Loc(), clip)
295                 v.Cursor.SetLoc(v.Cursor.Loc() + Count(clip))
296                 v.freshClip = false
297         case *tcell.EventMouse:
298                 x, y := e.Position()
299                 x -= v.lineNumOffset - v.leftCol
300                 y += v.Topline
301
302                 button := e.Buttons()
303
304                 switch button {
305                 case tcell.Button1:
306                         // Left click
307                         if v.mouseReleased && !e.HasMotion() {
308                                 v.MoveToMouseClick(x, y)
309                                 if time.Since(v.lastClickTime)/time.Millisecond < doubleClickThreshold {
310                                         if v.doubleClick {
311                                                 // Triple click
312                                                 v.lastClickTime = time.Now()
313
314                                                 v.tripleClick = true
315                                                 v.doubleClick = false
316
317                                                 v.Cursor.SelectLine()
318                                         } else {
319                                                 // Double click
320                                                 v.lastClickTime = time.Now()
321
322                                                 v.doubleClick = true
323                                                 v.tripleClick = false
324
325                                                 v.Cursor.SelectWord()
326                                         }
327                                 } else {
328                                         v.doubleClick = false
329                                         v.tripleClick = false
330                                         v.lastClickTime = time.Now()
331
332                                         loc := v.Cursor.Loc()
333                                         v.Cursor.origSelection[0] = loc
334                                         v.Cursor.curSelection[0] = loc
335                                         v.Cursor.curSelection[1] = loc
336                                 }
337                                 v.mouseReleased = false
338                         } else if !v.mouseReleased {
339                                 v.MoveToMouseClick(x, y)
340                                 if v.tripleClick {
341                                         v.Cursor.AddLineToSelection()
342                                 } else if v.doubleClick {
343                                         v.Cursor.AddWordToSelection()
344                                 } else {
345                                         v.Cursor.curSelection[1] = v.Cursor.Loc()
346                                 }
347                         }
348                 case tcell.ButtonNone:
349                         // Mouse event with no click
350                         if !v.mouseReleased {
351                                 // Mouse was just released
352
353                                 // Relocating here isn't really necessary because the cursor will
354                                 // be in the right place from the last mouse event
355                                 // However, if we are running in a terminal that doesn't support mouse motion
356                                 // events, this still allows the user to make selections, except only after they
357                                 // release the mouse
358
359                                 if !v.doubleClick && !v.tripleClick {
360                                         v.MoveToMouseClick(x, y)
361                                         v.Cursor.curSelection[1] = v.Cursor.Loc()
362                                 }
363                                 v.mouseReleased = true
364                         }
365                         // We don't want to relocate because otherwise the view will be relocated
366                         // every time the user moves the cursor
367                         relocate = false
368                 case tcell.WheelUp:
369                         // Scroll up
370                         scrollSpeed := int(settings["scrollspeed"].(float64))
371                         v.ScrollUp(scrollSpeed)
372                         // We don't want to relocate if the user is scrolling
373                         relocate = false
374                 case tcell.WheelDown:
375                         // Scroll down
376                         scrollSpeed := int(settings["scrollspeed"].(float64))
377                         v.ScrollDown(scrollSpeed)
378                         // We don't want to relocate if the user is scrolling
379                         relocate = false
380                 }
381         }
382
383         if relocate {
384                 v.Relocate()
385         }
386         if settings["syntax"].(bool) {
387                 v.matches = Match(v)
388         }
389 }
390
391 // GutterMessage creates a message in this view's gutter
392 func (v *View) GutterMessage(section string, lineN int, msg string, kind int) {
393         lineN--
394         gutterMsg := GutterMessage{
395                 lineNum: lineN,
396                 msg:     msg,
397                 kind:    kind,
398         }
399         for _, v := range v.messages {
400                 for _, gmsg := range v {
401                         if gmsg.lineNum == lineN {
402                                 return
403                         }
404                 }
405         }
406         messages := v.messages[section]
407         v.messages[section] = append(messages, gutterMsg)
408 }
409
410 // ClearGutterMessages clears all gutter messages from a given section
411 func (v *View) ClearGutterMessages(section string) {
412         v.messages[section] = []GutterMessage{}
413 }
414
415 // ClearAllGutterMessages clears all the gutter messages
416 func (v *View) ClearAllGutterMessages() {
417         for k := range v.messages {
418                 v.messages[k] = []GutterMessage{}
419         }
420 }
421
422 // DisplayView renders the view to the screen
423 func (v *View) DisplayView() {
424         // The character number of the character in the top left of the screen
425         charNum := ToCharPos(0, v.Topline, v.Buf)
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(v.Buf.NumLines))
430         // + 1 for the little space after the line number
431         if settings["ruler"] == true {
432                 v.lineNumOffset = maxLineLength + 1
433         } else {
434                 v.lineNumOffset = 0
435         }
436         var highlightStyle tcell.Style
437
438         var hasGutterMessages bool
439         for _, v := range v.messages {
440                 if len(v) > 0 {
441                         hasGutterMessages = true
442                 }
443         }
444         if hasGutterMessages {
445                 v.lineNumOffset += 2
446         }
447
448         for lineN := 0; lineN < v.height; lineN++ {
449                 var x int
450                 // If the buffer is smaller than the view height
451                 // and we went too far, break
452                 if lineN+v.Topline >= v.Buf.NumLines {
453                         break
454                 }
455                 line := v.Buf.Lines[lineN+v.Topline]
456
457                 if hasGutterMessages {
458                         msgOnLine := false
459                         for k := range v.messages {
460                                 for _, msg := range v.messages[k] {
461                                         if msg.lineNum == lineN+v.Topline {
462                                                 msgOnLine = true
463                                                 gutterStyle := tcell.StyleDefault
464                                                 switch msg.kind {
465                                                 case GutterInfo:
466                                                         if style, ok := colorscheme["gutter-info"]; ok {
467                                                                 gutterStyle = style
468                                                         }
469                                                 case GutterWarning:
470                                                         if style, ok := colorscheme["gutter-warning"]; ok {
471                                                                 gutterStyle = style
472                                                         }
473                                                 case GutterError:
474                                                         if style, ok := colorscheme["gutter-error"]; ok {
475                                                                 gutterStyle = style
476                                                         }
477                                                 }
478                                                 screen.SetContent(x, lineN, '>', nil, gutterStyle)
479                                                 x++
480                                                 screen.SetContent(x, lineN, '>', nil, gutterStyle)
481                                                 x++
482                                                 if v.Cursor.y == lineN+v.Topline {
483                                                         messenger.Message(msg.msg)
484                                                         messenger.gutterMessage = true
485                                                 }
486                                         }
487                                 }
488                         }
489                         if !msgOnLine {
490                                 screen.SetContent(x, lineN, ' ', nil, tcell.StyleDefault)
491                                 x++
492                                 screen.SetContent(x, lineN, ' ', nil, tcell.StyleDefault)
493                                 x++
494                                 if v.Cursor.y == lineN+v.Topline && messenger.gutterMessage {
495                                         messenger.Reset()
496                                         messenger.gutterMessage = false
497                                 }
498                         }
499                 }
500
501                 // Write the line number
502                 lineNumStyle := defStyle
503                 if style, ok := colorscheme["line-number"]; ok {
504                         lineNumStyle = style
505                 }
506                 // Write the spaces before the line number if necessary
507                 var lineNum string
508                 if settings["ruler"] == true {
509                         lineNum = strconv.Itoa(lineN + v.Topline + 1)
510                         for i := 0; i < maxLineLength-len(lineNum); i++ {
511                                 screen.SetContent(x, lineN, ' ', nil, lineNumStyle)
512                                 x++
513                         }
514                         // Write the actual line number
515                         for _, ch := range lineNum {
516                                 screen.SetContent(x, lineN, ch, nil, lineNumStyle)
517                                 x++
518                         }
519
520                         if settings["ruler"] == true {
521                                 // Write the extra space
522                                 screen.SetContent(x, lineN, ' ', nil, lineNumStyle)
523                                 x++
524                         }
525                 }
526                 // Write the line
527                 tabchars := 0
528                 for colN, ch := range line {
529                         var lineStyle tcell.Style
530
531                         if settings["syntax"].(bool) {
532                                 // Syntax highlighting is enabled
533                                 highlightStyle = v.matches[lineN][colN]
534                         }
535
536                         if v.Cursor.HasSelection() &&
537                                 (charNum >= v.Cursor.curSelection[0] && charNum < v.Cursor.curSelection[1] ||
538                                         charNum < v.Cursor.curSelection[0] && charNum >= v.Cursor.curSelection[1]) {
539
540                                 lineStyle = tcell.StyleDefault.Reverse(true)
541
542                                 if style, ok := colorscheme["selection"]; ok {
543                                         lineStyle = style
544                                 }
545                         } else {
546                                 lineStyle = highlightStyle
547                         }
548
549                         if ch == '\t' {
550                                 screen.SetContent(x+tabchars, lineN, ' ', nil, lineStyle)
551                                 tabSize := int(settings["tabsize"].(float64))
552                                 for i := 0; i < tabSize-1; i++ {
553                                         tabchars++
554                                         if x-v.leftCol+tabchars >= v.lineNumOffset {
555                                                 screen.SetContent(x-v.leftCol+tabchars, lineN, ' ', nil, lineStyle)
556                                         }
557                                 }
558                         } else {
559                                 if x-v.leftCol+tabchars >= v.lineNumOffset {
560                                         screen.SetContent(x-v.leftCol+tabchars, lineN, ch, nil, lineStyle)
561                                 }
562                         }
563                         charNum++
564                         x++
565                 }
566                 // Here we are at a newline
567
568                 // The newline may be selected, in which case we should draw the selection style
569                 // with a space to represent it
570                 if v.Cursor.HasSelection() &&
571                         (charNum >= v.Cursor.curSelection[0] && charNum < v.Cursor.curSelection[1] ||
572                                 charNum < v.Cursor.curSelection[0] && charNum >= v.Cursor.curSelection[1]) {
573
574                         selectStyle := defStyle.Reverse(true)
575
576                         if style, ok := colorscheme["selection"]; ok {
577                                 selectStyle = style
578                         }
579                         screen.SetContent(x-v.leftCol+tabchars, lineN, ' ', nil, selectStyle)
580                 }
581
582                 charNum++
583         }
584 }
585
586 // Display renders the view, the cursor, and statusline
587 func (v *View) Display() {
588         v.DisplayView()
589         v.Cursor.Display()
590         if settings["statusline"].(bool) {
591                 v.sline.Display()
592         }
593 }