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