]> git.lizzy.rs Git - micro.git/blob - cmd/micro/view.go
Use a map for settings instead of a struct
[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 has a 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 <= len(v.buf.lines)-v.height {
125                 v.topline += n
126         } else if v.topline < len(v.buf.lines)-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 >= len(v.buf.lines) {
227                 y = len(v.buf.lines) - 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.EventMouse:
272                 x, y := e.Position()
273                 x -= v.lineNumOffset - v.leftCol
274                 y += v.topline
275
276                 button := e.Buttons()
277
278                 switch button {
279                 case tcell.Button1:
280                         // Left click
281                         origX, origY := v.cursor.x, v.cursor.y
282
283                         if v.mouseReleased && !e.HasMotion() {
284                                 v.MoveToMouseClick(x, y)
285                                 if (time.Since(v.lastClickTime)/time.Millisecond < doubleClickThreshold) &&
286                                         (origX == v.cursor.x && origY == v.cursor.y) {
287                                         if v.doubleClick {
288                                                 // Triple click
289                                                 v.lastClickTime = time.Now()
290
291                                                 v.tripleClick = true
292                                                 v.doubleClick = false
293
294                                                 v.cursor.SelectLine()
295                                         } else {
296                                                 // Double click
297                                                 v.lastClickTime = time.Now()
298
299                                                 v.doubleClick = true
300                                                 v.tripleClick = false
301
302                                                 v.cursor.SelectWord()
303                                         }
304                                 } else {
305                                         v.doubleClick = false
306                                         v.tripleClick = false
307                                         v.lastClickTime = time.Now()
308
309                                         loc := v.cursor.Loc()
310                                         v.cursor.curSelection[0] = loc
311                                         v.cursor.curSelection[1] = loc
312                                 }
313                                 v.mouseReleased = false
314                         } else if !v.mouseReleased {
315                                 v.MoveToMouseClick(x, y)
316                                 if v.tripleClick {
317                                         v.cursor.AddLineToSelection()
318                                 } else if v.doubleClick {
319                                         v.cursor.AddWordToSelection()
320                                 } else {
321                                         v.cursor.curSelection[1] = v.cursor.Loc()
322                                 }
323                         }
324                 case tcell.ButtonNone:
325                         // Mouse event with no click
326                         if !v.mouseReleased {
327                                 // Mouse was just released
328
329                                 // Relocating here isn't really necessary because the cursor will
330                                 // be in the right place from the last mouse event
331                                 // However, if we are running in a terminal that doesn't support mouse motion
332                                 // events, this still allows the user to make selections, except only after they
333                                 // release the mouse
334
335                                 if !v.doubleClick && !v.tripleClick {
336                                         v.MoveToMouseClick(x, y)
337                                         v.cursor.curSelection[1] = v.cursor.Loc()
338                                 }
339                                 v.mouseReleased = true
340                         }
341                         // We don't want to relocate because otherwise the view will be relocated
342                         // every time the user moves the cursor
343                         relocate = false
344                 case tcell.WheelUp:
345                         // Scroll up two lines
346                         v.ScrollUp(2)
347                         // We don't want to relocate if the user is scrolling
348                         relocate = false
349                 case tcell.WheelDown:
350                         // Scroll down two lines
351                         v.ScrollDown(2)
352                         // We don't want to relocate if the user is scrolling
353                         relocate = false
354                 }
355         }
356
357         if relocate {
358                 v.Relocate()
359         }
360         if settings["syntax"].(bool) {
361                 v.matches = Match(v)
362         }
363 }
364
365 // GutterMessage creates a message in this view's gutter
366 func (v *View) GutterMessage(lineN int, msg string, kind int) {
367         gutterMsg := GutterMessage{
368                 lineNum: lineN,
369                 msg:     msg,
370                 kind:    kind,
371         }
372         for _, gmsg := range v.messages {
373                 if gmsg.lineNum == lineN {
374                         return
375                 }
376         }
377         v.messages = append(v.messages, gutterMsg)
378 }
379
380 // DisplayView renders the view to the screen
381 func (v *View) DisplayView() {
382         // The character number of the character in the top left of the screen
383         charNum := ToCharPos(0, v.topline, v.buf)
384
385         // Convert the length of buffer to a string, and get the length of the string
386         // We are going to have to offset by that amount
387         maxLineLength := len(strconv.Itoa(len(v.buf.lines)))
388         // + 1 for the little space after the line number
389         if settings["ruler"] == true {
390                 v.lineNumOffset = maxLineLength + 1
391         } else {
392                 v.lineNumOffset = 0
393         }
394         var highlightStyle tcell.Style
395
396         if len(v.messages) > 0 {
397                 v.lineNumOffset += 2
398         }
399
400         for lineN := 0; lineN < v.height; lineN++ {
401                 var x int
402                 // If the buffer is smaller than the view height
403                 // and we went too far, break
404                 if lineN+v.topline >= len(v.buf.lines) {
405                         break
406                 }
407                 line := v.buf.lines[lineN+v.topline]
408
409                 if len(v.messages) > 0 {
410                         msgOnLine := false
411                         for _, msg := range v.messages {
412                                 if msg.lineNum == lineN+v.topline {
413                                         msgOnLine = true
414                                         gutterStyle := tcell.StyleDefault
415                                         switch msg.kind {
416                                         case GutterInfo:
417                                                 if style, ok := colorscheme["gutter-info"]; ok {
418                                                         gutterStyle = style
419                                                 }
420                                         case GutterWarning:
421                                                 if style, ok := colorscheme["gutter-warning"]; ok {
422                                                         gutterStyle = style
423                                                 }
424                                         case GutterError:
425                                                 if style, ok := colorscheme["gutter-error"]; ok {
426                                                         gutterStyle = style
427                                                 }
428                                         }
429                                         screen.SetContent(x, lineN, '>', nil, gutterStyle)
430                                         x++
431                                         screen.SetContent(x, lineN, '>', nil, gutterStyle)
432                                         x++
433                                         if v.cursor.y == lineN {
434                                                 messenger.Message(msg.msg)
435                                                 messenger.gutterMessage = true
436                                         }
437                                 }
438                         }
439                         if !msgOnLine {
440                                 screen.SetContent(x, lineN, ' ', nil, tcell.StyleDefault)
441                                 x++
442                                 screen.SetContent(x, lineN, ' ', nil, tcell.StyleDefault)
443                                 x++
444                                 if v.cursor.y == lineN && messenger.gutterMessage {
445                                         messenger.Reset()
446                                         messenger.gutterMessage = false
447                                 }
448                         }
449                 }
450
451                 // Write the line number
452                 lineNumStyle := defStyle
453                 if style, ok := colorscheme["line-number"]; ok {
454                         lineNumStyle = style
455                 }
456                 // Write the spaces before the line number if necessary
457                 var lineNum string
458                 if settings["ruler"] == true {
459                         lineNum = strconv.Itoa(lineN + v.topline + 1)
460                         for i := 0; i < maxLineLength-len(lineNum); i++ {
461                                 screen.SetContent(x, lineN, ' ', nil, lineNumStyle)
462                                 x++
463                         }
464                         // Write the actual line number
465                         for _, ch := range lineNum {
466                                 screen.SetContent(x, lineN, ch, nil, lineNumStyle)
467                                 x++
468                         }
469
470                         if settings["ruler"] == true {
471                                 // Write the extra space
472                                 screen.SetContent(x, lineN, ' ', nil, lineNumStyle)
473                                 x++
474                         }
475                 }
476                 // Write the line
477                 tabchars := 0
478                 for colN, ch := range line {
479                         var lineStyle tcell.Style
480
481                         if settings["syntax"].(bool) {
482                                 // Syntax highlighting is enabled
483                                 highlightStyle = v.matches[lineN][colN]
484                         }
485
486                         if v.cursor.HasSelection() &&
487                                 (charNum >= v.cursor.curSelection[0] && charNum < v.cursor.curSelection[1] ||
488                                         charNum < v.cursor.curSelection[0] && charNum >= v.cursor.curSelection[1]) {
489
490                                 lineStyle = tcell.StyleDefault.Reverse(true)
491
492                                 if style, ok := colorscheme["selection"]; ok {
493                                         lineStyle = style
494                                 }
495                         } else {
496                                 lineStyle = highlightStyle
497                         }
498
499                         if ch == '\t' {
500                                 screen.SetContent(x+tabchars, lineN, ' ', nil, lineStyle)
501                                 tabSize := int(settings["tabsize"].(float64))
502                                 for i := 0; i < tabSize-1; i++ {
503                                         tabchars++
504                                         if x-v.leftCol+tabchars >= v.lineNumOffset {
505                                                 screen.SetContent(x-v.leftCol+tabchars, lineN, ' ', nil, lineStyle)
506                                         }
507                                 }
508                         } else {
509                                 if x-v.leftCol+tabchars >= v.lineNumOffset {
510                                         screen.SetContent(x-v.leftCol+tabchars, lineN, ch, nil, lineStyle)
511                                 }
512                         }
513                         charNum++
514                         x++
515                 }
516                 // Here we are at a newline
517
518                 // The newline may be selected, in which case we should draw the selection style
519                 // with a space to represent it
520                 if v.cursor.HasSelection() &&
521                         (charNum >= v.cursor.curSelection[0] && charNum < v.cursor.curSelection[1] ||
522                                 charNum < v.cursor.curSelection[0] && charNum >= v.cursor.curSelection[1]) {
523
524                         selectStyle := defStyle.Reverse(true)
525
526                         if style, ok := colorscheme["selection"]; ok {
527                                 selectStyle = style
528                         }
529                         screen.SetContent(x-v.leftCol+tabchars, lineN, ' ', nil, selectStyle)
530                 }
531
532                 charNum++
533         }
534 }
535
536 // Display renders the view, the cursor, and statusline
537 func (v *View) Display() {
538         v.DisplayView()
539         v.cursor.Display()
540         v.sline.Display()
541 }