]> git.lizzy.rs Git - micro.git/blob - cmd/micro/view.go
Add a bit of "padding" to relocate
[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 - 4
210                 ret = true
211         }
212         if cy > v.Topline+v.height-1 {
213                 v.Topline = cy - v.height + 5
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.keyCode {
277                                         if e.Modifiers() == key.modifiers {
278                                                 relocate = action(v)
279                                                 for _, pl := range loadedPlugins {
280                                                         funcName := strings.Split(runtime.FuncForPC(reflect.ValueOf(action).Pointer()).Name(), ".")
281                                                         err := Call(pl + "_on" + funcName[len(funcName)-1])
282                                                         if err != nil {
283                                                                 TermMessage(err)
284                                                         }
285                                                 }
286                                         }
287                                 }
288                         }
289                 }
290         case *tcell.EventPaste:
291                 if v.Cursor.HasSelection() {
292                         v.Cursor.DeleteSelection()
293                         v.Cursor.ResetSelection()
294                 }
295                 clip := e.Text()
296                 v.eh.Insert(v.Cursor.Loc(), clip)
297                 v.Cursor.SetLoc(v.Cursor.Loc() + Count(clip))
298                 v.freshClip = false
299         case *tcell.EventMouse:
300                 x, y := e.Position()
301                 x -= v.lineNumOffset - v.leftCol
302                 y += v.Topline
303                 // Don't relocate for mouse events
304                 relocate = false
305
306                 button := e.Buttons()
307
308                 switch button {
309                 case tcell.Button1:
310                         // Left click
311                         if v.mouseReleased {
312                                 v.MoveToMouseClick(x, y)
313                                 if time.Since(v.lastClickTime)/time.Millisecond < doubleClickThreshold {
314                                         if v.doubleClick {
315                                                 // Triple click
316                                                 v.lastClickTime = time.Now()
317
318                                                 v.tripleClick = true
319                                                 v.doubleClick = false
320
321                                                 v.Cursor.SelectLine()
322                                         } else {
323                                                 // Double click
324                                                 v.lastClickTime = time.Now()
325
326                                                 v.doubleClick = true
327                                                 v.tripleClick = false
328
329                                                 v.Cursor.SelectWord()
330                                         }
331                                 } else {
332                                         v.doubleClick = false
333                                         v.tripleClick = false
334                                         v.lastClickTime = time.Now()
335
336                                         loc := v.Cursor.Loc()
337                                         v.Cursor.origSelection[0] = loc
338                                         v.Cursor.curSelection[0] = loc
339                                         v.Cursor.curSelection[1] = loc
340                                 }
341                                 v.mouseReleased = false
342                         } else if !v.mouseReleased {
343                                 v.MoveToMouseClick(x, y)
344                                 if v.tripleClick {
345                                         v.Cursor.AddLineToSelection()
346                                 } else if v.doubleClick {
347                                         v.Cursor.AddWordToSelection()
348                                 } else {
349                                         v.Cursor.curSelection[1] = v.Cursor.Loc()
350                                 }
351                         }
352                 case tcell.ButtonNone:
353                         // Mouse event with no click
354                         if !v.mouseReleased {
355                                 // Mouse was just released
356
357                                 // Relocating here isn't really necessary because the cursor will
358                                 // be in the right place from the last mouse event
359                                 // However, if we are running in a terminal that doesn't support mouse motion
360                                 // events, this still allows the user to make selections, except only after they
361                                 // release the mouse
362
363                                 if !v.doubleClick && !v.tripleClick {
364                                         v.MoveToMouseClick(x, y)
365                                         v.Cursor.curSelection[1] = v.Cursor.Loc()
366                                 }
367                                 v.mouseReleased = true
368                         }
369                 case tcell.WheelUp:
370                         // Scroll up
371                         scrollSpeed := int(settings["scrollSpeed"].(float64))
372                         v.ScrollUp(scrollSpeed)
373                 case tcell.WheelDown:
374                         // Scroll down
375                         scrollSpeed := int(settings["scrollSpeed"].(float64))
376                         v.ScrollDown(scrollSpeed)
377                 }
378         }
379
380         if relocate {
381                 v.Relocate()
382         }
383         if settings["syntax"].(bool) {
384                 v.matches = Match(v)
385         }
386 }
387
388 // GutterMessage creates a message in this view's gutter
389 func (v *View) GutterMessage(section string, lineN int, msg string, kind int) {
390         lineN--
391         gutterMsg := GutterMessage{
392                 lineNum: lineN,
393                 msg:     msg,
394                 kind:    kind,
395         }
396         for _, v := range v.messages {
397                 for _, gmsg := range v {
398                         if gmsg.lineNum == lineN {
399                                 return
400                         }
401                 }
402         }
403         messages := v.messages[section]
404         v.messages[section] = append(messages, gutterMsg)
405 }
406
407 // ClearGutterMessages clears all gutter messages from a given section
408 func (v *View) ClearGutterMessages(section string) {
409         v.messages[section] = []GutterMessage{}
410 }
411
412 // ClearAllGutterMessages clears all the gutter messages
413 func (v *View) ClearAllGutterMessages() {
414         for k := range v.messages {
415                 v.messages[k] = []GutterMessage{}
416         }
417 }
418
419 // DisplayView renders the view to the screen
420 func (v *View) DisplayView() {
421         // The character number of the character in the top left of the screen
422         charNum := ToCharPos(0, v.Topline, v.Buf)
423
424         // Convert the length of buffer to a string, and get the length of the string
425         // We are going to have to offset by that amount
426         maxLineLength := len(strconv.Itoa(v.Buf.NumLines))
427         // + 1 for the little space after the line number
428         if settings["ruler"] == true {
429                 v.lineNumOffset = maxLineLength + 1
430         } else {
431                 v.lineNumOffset = 0
432         }
433         var highlightStyle tcell.Style
434
435         var hasGutterMessages bool
436         for _, v := range v.messages {
437                 if len(v) > 0 {
438                         hasGutterMessages = true
439                 }
440         }
441         if hasGutterMessages {
442                 v.lineNumOffset += 2
443         }
444
445         for lineN := 0; lineN < v.height; lineN++ {
446                 var x int
447                 // If the buffer is smaller than the view height
448                 // and we went too far, break
449                 if lineN+v.Topline >= v.Buf.NumLines {
450                         break
451                 }
452                 line := v.Buf.Lines[lineN+v.Topline]
453
454                 if hasGutterMessages {
455                         msgOnLine := false
456                         for k := range v.messages {
457                                 for _, msg := range v.messages[k] {
458                                         if msg.lineNum == lineN+v.Topline {
459                                                 msgOnLine = true
460                                                 gutterStyle := tcell.StyleDefault
461                                                 switch msg.kind {
462                                                 case GutterInfo:
463                                                         if style, ok := colorscheme["gutter-info"]; ok {
464                                                                 gutterStyle = style
465                                                         }
466                                                 case GutterWarning:
467                                                         if style, ok := colorscheme["gutter-warning"]; ok {
468                                                                 gutterStyle = style
469                                                         }
470                                                 case GutterError:
471                                                         if style, ok := colorscheme["gutter-error"]; ok {
472                                                                 gutterStyle = style
473                                                         }
474                                                 }
475                                                 screen.SetContent(x, lineN, '>', nil, gutterStyle)
476                                                 x++
477                                                 screen.SetContent(x, lineN, '>', nil, gutterStyle)
478                                                 x++
479                                                 if v.Cursor.y == lineN+v.Topline {
480                                                         messenger.Message(msg.msg)
481                                                         messenger.gutterMessage = true
482                                                 }
483                                         }
484                                 }
485                         }
486                         if !msgOnLine {
487                                 screen.SetContent(x, lineN, ' ', nil, tcell.StyleDefault)
488                                 x++
489                                 screen.SetContent(x, lineN, ' ', nil, tcell.StyleDefault)
490                                 x++
491                                 if v.Cursor.y == lineN+v.Topline && messenger.gutterMessage {
492                                         messenger.Reset()
493                                         messenger.gutterMessage = false
494                                 }
495                         }
496                 }
497
498                 // Write the line number
499                 lineNumStyle := defStyle
500                 if style, ok := colorscheme["line-number"]; ok {
501                         lineNumStyle = style
502                 }
503                 // Write the spaces before the line number if necessary
504                 var lineNum string
505                 if settings["ruler"] == true {
506                         lineNum = strconv.Itoa(lineN + v.Topline + 1)
507                         for i := 0; i < maxLineLength-len(lineNum); i++ {
508                                 screen.SetContent(x, lineN, ' ', nil, lineNumStyle)
509                                 x++
510                         }
511                         // Write the actual line number
512                         for _, ch := range lineNum {
513                                 screen.SetContent(x, lineN, ch, nil, lineNumStyle)
514                                 x++
515                         }
516
517                         if settings["ruler"] == true {
518                                 // Write the extra space
519                                 screen.SetContent(x, lineN, ' ', nil, lineNumStyle)
520                                 x++
521                         }
522                 }
523                 // Write the line
524                 tabchars := 0
525                 for colN, ch := range line {
526                         var lineStyle tcell.Style
527
528                         if settings["syntax"].(bool) {
529                                 // Syntax highlighting is enabled
530                                 highlightStyle = v.matches[lineN][colN]
531                         }
532
533                         if v.Cursor.HasSelection() &&
534                                 (charNum >= v.Cursor.curSelection[0] && charNum < v.Cursor.curSelection[1] ||
535                                         charNum < v.Cursor.curSelection[0] && charNum >= v.Cursor.curSelection[1]) {
536
537                                 lineStyle = tcell.StyleDefault.Reverse(true)
538
539                                 if style, ok := colorscheme["selection"]; ok {
540                                         lineStyle = style
541                                 }
542                         } else {
543                                 lineStyle = highlightStyle
544                         }
545
546                         if ch == '\t' {
547                                 lineIndentStyle := defStyle
548                                 if style, ok := colorscheme["indent-char"]; ok {
549                                         lineIndentStyle = style
550                                 }
551                                 if v.Cursor.HasSelection() &&
552                                         (charNum >= v.Cursor.curSelection[0] && charNum < v.Cursor.curSelection[1] ||
553                                                 charNum < v.Cursor.curSelection[0] && charNum >= v.Cursor.curSelection[1]) {
554
555                                         lineIndentStyle = tcell.StyleDefault.Reverse(true)
556
557                                         if style, ok := colorscheme["selection"]; ok {
558                                                 lineIndentStyle = style
559                                         }
560                                 }
561                                 indentChar := []rune(settings["indentchar"].(string))
562                                 screen.SetContent(x-v.leftCol+tabchars, lineN, indentChar[0], nil, lineIndentStyle)
563                                 tabSize := int(settings["tabsize"].(float64))
564                                 for i := 0; i < tabSize-1; i++ {
565                                         tabchars++
566                                         if x-v.leftCol+tabchars >= v.lineNumOffset {
567                                                 screen.SetContent(x-v.leftCol+tabchars, lineN, ' ', nil, lineStyle)
568                                         }
569                                 }
570                         } else {
571                                 if x-v.leftCol+tabchars >= v.lineNumOffset {
572                                         screen.SetContent(x-v.leftCol+tabchars, lineN, ch, nil, lineStyle)
573                                 }
574                         }
575                         charNum++
576                         x++
577                 }
578                 // Here we are at a newline
579
580                 // The newline may be selected, in which case we should draw the selection style
581                 // with a space to represent it
582                 if v.Cursor.HasSelection() &&
583                         (charNum >= v.Cursor.curSelection[0] && charNum < v.Cursor.curSelection[1] ||
584                                 charNum < v.Cursor.curSelection[0] && charNum >= v.Cursor.curSelection[1]) {
585
586                         selectStyle := defStyle.Reverse(true)
587
588                         if style, ok := colorscheme["selection"]; ok {
589                                 selectStyle = style
590                         }
591                         screen.SetContent(x-v.leftCol+tabchars, lineN, ' ', nil, selectStyle)
592                 }
593
594                 charNum++
595         }
596 }
597
598 // Display renders the view, the cursor, and statusline
599 func (v *View) Display() {
600         v.DisplayView()
601         v.Cursor.Display()
602         if settings["statusline"].(bool) {
603                 v.sline.Display()
604         }
605 }