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