]> git.lizzy.rs Git - micro.git/blobdiff - src/view.go
Slight cleanup
[micro.git] / src / view.go
index 971d5a540a3f9486d43b5ebf181bbeb0c6e211e9..1070459555b7d08be8f6e8cd1c32e83b33fd23cd 100644 (file)
@@ -2,58 +2,80 @@ package main
 
 import (
        "github.com/atotto/clipboard"
-       "github.com/zyedidia/tcell"
+       "github.com/gdamore/tcell"
+       "io/ioutil"
        "strconv"
+       "strings"
 )
 
 // The View struct stores information about a view into a buffer.
 // It has a value for the cursor, and the window that the user sees
 // the buffer from.
 type View struct {
-       cursor  Cursor
+       cursor Cursor
+
+       // The topmost line, used for vertical scrolling
        topline int
-       // Leftmost column. Used for horizontal scrolling
+       // The leftmost column, used for horizontal scrolling
        leftCol int
 
-       // Percentage of the terminal window that this view takes up
-       heightPercent float32
-       widthPercent  float32
-       height        int
-       width         int
+       // Percentage of the terminal window that this view takes up (from 0 to 100)
+       widthPercent  int
+       heightPercent int
+
+       // Actual with and height
+       width  int
+       height int
 
        // How much to offset because of line numbers
        lineNumOffset int
 
+       // The eventhandler for undo/redo
        eh *EventHandler
 
+       // The buffer
        buf *Buffer
-       sl  Statusline
+       // The statusline
+       sline Statusline
 
+       // Since tcell doesn't differentiate between a mouse release event
+       // and a mouse move event with no keys pressed, we need to keep
+       // track of whether or not the mouse was pressed (or not released) last event to determine
+       // mouse release events
        mouseReleased bool
 
        // Syntax highlighting matches
-       matches map[int]tcell.Style
+       matches SyntaxMatches
+       // The matches from the last frame
+       lastMatches SyntaxMatches
+
+       // This is the range of lines that should have their syntax highlighting updated
+       updateLines [2]int
 
-       s tcell.Screen
+       // The messenger so we can send messages to the user and get input from them
+       m *Messenger
 }
 
-// NewView returns a new view with fullscreen width and height
-func NewView(buf *Buffer, s tcell.Screen) *View {
-       return NewViewWidthHeight(buf, s, 1, 1)
+// NewView returns a new fullscreen view
+func NewView(buf *Buffer, m *Messenger) *View {
+       return NewViewWidthHeight(buf, m, 100, 100)
 }
 
 // NewViewWidthHeight returns a new view with the specified width and height percentages
-func NewViewWidthHeight(buf *Buffer, s tcell.Screen, w, h float32) *View {
+// Note that w and h are percentages not actual values
+func NewViewWidthHeight(buf *Buffer, m *Messenger, w, h int) *View {
        v := new(View)
 
        v.buf = buf
-       v.s = s
+       // Messenger
+       v.m = m
 
        v.widthPercent = w
        v.heightPercent = h
-       v.Resize(s.Size())
+       v.Resize(screen.Size())
 
        v.topline = 0
+       // Put the cursor at the first spot
        v.cursor = Cursor{
                x:   0,
                y:   0,
@@ -63,18 +85,37 @@ func NewViewWidthHeight(buf *Buffer, s tcell.Screen, w, h float32) *View {
 
        v.eh = NewEventHandler(v)
 
-       v.sl = Statusline{
-               v: v,
+       v.sline = Statusline{
+               view: v,
        }
 
+       // Update the syntax highlighting for the entire buffer at the start
+       v.UpdateLines(v.topline, v.topline+v.height)
+       // v.matches = Match(v.buf.rules, v.buf, v)
+
+       // Set mouseReleased to true because we assume the mouse is not being pressed when
+       // the editor is opened
+       v.mouseReleased = true
+
        return v
 }
 
-// Resize recalculates the width and height of the view based on the width and height percentages
+// UpdateLines sets the values for v.updateLines
+func (v *View) UpdateLines(start, end int) {
+       v.updateLines[0] = start
+       v.updateLines[1] = end
+}
+
+// Resize recalculates the actual width and height of the view from the width and height
+// percentages
+// This is usually called when the window is resized, or when a split has been added and
+// the percentages have changed
 func (v *View) Resize(w, h int) {
+       // Always include 1 line for the command line at the bottom
        h--
-       v.height = int(float32(h)*v.heightPercent) - 1
-       v.width = int(float32(w) * v.widthPercent)
+       v.width = int(float32(w) * float32(v.widthPercent) / 100)
+       // We subtract 1 for the statusline
+       v.height = int(float32(h)*float32(v.heightPercent)/100) - 1
 }
 
 // ScrollUp scrolls the view up n lines (if possible)
@@ -133,44 +174,191 @@ func (v *View) HalfPageDown() {
        }
 }
 
+// CanClose returns whether or not the view can be closed
+// If there are unsaved changes, the user will be asked if the view can be closed
+// causing them to lose the unsaved changes
+// The message is what to print after saying "You have unsaved changes. "
+func (v *View) CanClose(msg string) bool {
+       if v.buf.IsDirty() {
+               quit, canceled := v.m.Prompt("You have unsaved changes. " + msg)
+               if !canceled {
+                       if strings.ToLower(quit) == "yes" || strings.ToLower(quit) == "y" {
+                               return true
+                       }
+               }
+       } else {
+               return true
+       }
+       return false
+}
+
+// Save the buffer to disk
+func (v *View) Save() {
+       // If this is an empty buffer, ask for a filename
+       if v.buf.path == "" {
+               filename, canceled := v.m.Prompt("Filename: ")
+               if !canceled {
+                       v.buf.path = filename
+                       v.buf.name = filename
+               } else {
+                       return
+               }
+       }
+       err := v.buf.Save()
+       if err != nil {
+               v.m.Error(err.Error())
+       }
+}
+
+// Copy the selection to the system clipboard
+func (v *View) Copy() {
+       if v.cursor.HasSelection() {
+               if !clipboard.Unsupported {
+                       clipboard.WriteAll(v.cursor.GetSelection())
+               } else {
+                       v.m.Error("Clipboard is not supported on your system")
+               }
+       }
+}
+
+// Cut the selection to the system clipboard
+func (v *View) Cut() {
+       if v.cursor.HasSelection() {
+               if !clipboard.Unsupported {
+                       clipboard.WriteAll(v.cursor.GetSelection())
+                       v.cursor.DeleteSelection()
+                       v.cursor.ResetSelection()
+               } else {
+                       v.m.Error("Clipboard is not supported on your system")
+               }
+       }
+}
+
+// Paste whatever is in the system clipboard into the buffer
+// Delete and paste if the user has a selection
+func (v *View) Paste() {
+       if !clipboard.Unsupported {
+               if v.cursor.HasSelection() {
+                       v.cursor.DeleteSelection()
+                       v.cursor.ResetSelection()
+               }
+               clip, _ := clipboard.ReadAll()
+               v.eh.Insert(v.cursor.loc, clip)
+               // This is a bit weird... Not sure if there's a better way
+               for i := 0; i < Count(clip); i++ {
+                       v.cursor.Right()
+               }
+       } else {
+               v.m.Error("Clipboard is not supported on your system")
+       }
+}
+
+// SelectAll selects the entire buffer
+func (v *View) SelectAll() {
+       v.cursor.selectionEnd = 0
+       v.cursor.selectionStart = v.buf.Len()
+       // Put the cursor at the beginning
+       v.cursor.x = 0
+       v.cursor.y = 0
+       v.cursor.loc = 0
+}
+
+// OpenFile opens a new file in the current view
+// It makes sure that the current buffer can be closed first (unsaved changes)
+func (v *View) OpenFile() {
+       if v.CanClose("Continue? ") {
+               filename, canceled := v.m.Prompt("File to open: ")
+               if canceled {
+                       return
+               }
+               file, err := ioutil.ReadFile(filename)
+
+               if err != nil {
+                       v.m.Error(err.Error())
+                       return
+               }
+               v.buf = NewBuffer(string(file), filename)
+       }
+}
+
+// Relocate moves the view window so that the cursor is in view
+// This is useful if the user has scrolled far away, and then starts typing
+func (v *View) Relocate() {
+       cy := v.cursor.y
+       if cy < v.topline {
+               v.topline = cy
+       }
+       if cy > v.topline+v.height-1 {
+               v.topline = cy - v.height + 1
+       }
+}
+
+// MoveToMouseClick moves the cursor to location x, y assuming x, y were given
+// by a mouse click
+func (v *View) MoveToMouseClick(x, y int) {
+       if y-v.topline > v.height-1 {
+               v.ScrollDown(1)
+               y = v.height + v.topline - 1
+       }
+       if y >= len(v.buf.lines) {
+               y = len(v.buf.lines) - 1
+       }
+       if x < 0 {
+               x = 0
+       }
+
+       x = v.cursor.GetCharPosInLine(y, x)
+       if x > Count(v.buf.lines[y]) {
+               x = Count(v.buf.lines[y])
+       }
+       d := v.cursor.Distance(x, y)
+       v.cursor.loc += d
+       v.cursor.x = x
+       v.cursor.y = y
+}
+
 // HandleEvent handles an event passed by the main loop
-// It returns an int describing how the screen needs to be redrawn
-// 0: Screen does not need to be redrawn
-// 1: Only the cursor/statusline needs to be redrawn
-// 2: Everything needs to be redrawn
-func (v *View) HandleEvent(event tcell.Event) int {
-       var ret int
+func (v *View) HandleEvent(event tcell.Event) {
+       // This bool determines whether the view is relocated at the end of the function
+       // By default it's true because most events should cause a relocate
+       relocate := true
+       // By default we don't update and syntax highlighting
+       v.UpdateLines(-2, 0)
        switch e := event.(type) {
        case *tcell.EventResize:
+               // Window resized
                v.Resize(e.Size())
-               ret = 2
        case *tcell.EventKey:
                switch e.Key() {
                case tcell.KeyUp:
+                       // Cursor up
                        v.cursor.Up()
-                       ret = 1
                case tcell.KeyDown:
+                       // Cursor down
                        v.cursor.Down()
-                       ret = 1
                case tcell.KeyLeft:
+                       // Cursor left
                        v.cursor.Left()
-                       ret = 1
                case tcell.KeyRight:
+                       // Cursor right
                        v.cursor.Right()
-                       ret = 1
                case tcell.KeyEnter:
+                       // Insert a newline
                        v.eh.Insert(v.cursor.loc, "\n")
                        v.cursor.Right()
-                       ret = 2
+                       v.UpdateLines(v.cursor.y-1, v.cursor.y)
                case tcell.KeySpace:
+                       // Insert a space
                        v.eh.Insert(v.cursor.loc, " ")
                        v.cursor.Right()
-                       ret = 2
+                       v.UpdateLines(v.cursor.y, v.cursor.y)
                case tcell.KeyBackspace2:
+                       // Delete a character
                        if v.cursor.HasSelection() {
                                v.cursor.DeleteSelection()
                                v.cursor.ResetSelection()
-                               ret = 2
+                               // Rehighlight the entire buffer
+                               v.UpdateLines(v.topline, v.topline+v.height)
                        } else if v.cursor.loc > 0 {
                                // We have to do something a bit hacky here because we want to
                                // delete the line by first moving left and then deleting backwards
@@ -182,75 +370,58 @@ func (v *View) HandleEvent(event tcell.Event) int {
                                v.cursor.Right()
                                v.eh.Remove(v.cursor.loc-1, v.cursor.loc)
                                v.cursor.x, v.cursor.y, v.cursor.loc = cx, cy, cloc
-                               ret = 2
+                               v.UpdateLines(v.cursor.y, v.cursor.y+1)
                        }
                case tcell.KeyTab:
+                       // Insert a tab
                        v.eh.Insert(v.cursor.loc, "\t")
                        v.cursor.Right()
-                       ret = 2
+                       v.UpdateLines(v.cursor.y, v.cursor.y)
                case tcell.KeyCtrlS:
-                       err := v.buf.Save()
-                       if err != nil {
-                               Error(err.Error())
-                       }
-                       // Need to redraw the status line
-                       ret = 1
+                       v.Save()
                case tcell.KeyCtrlZ:
                        v.eh.Undo()
-                       ret = 2
+                       // Rehighlight the entire buffer
+                       v.UpdateLines(v.topline, v.topline+v.height)
                case tcell.KeyCtrlY:
                        v.eh.Redo()
-                       ret = 2
+                       // Rehighlight the entire buffer
+                       v.UpdateLines(v.topline, v.topline+v.height)
                case tcell.KeyCtrlC:
-                       if v.cursor.HasSelection() {
-                               if !clipboard.Unsupported {
-                                       clipboard.WriteAll(v.cursor.GetSelection())
-                                       ret = 2
-                               }
-                       }
+                       v.Copy()
+                       // Rehighlight the entire buffer
+                       v.UpdateLines(v.topline, v.topline+v.height)
                case tcell.KeyCtrlX:
-                       if v.cursor.HasSelection() {
-                               if !clipboard.Unsupported {
-                                       clipboard.WriteAll(v.cursor.GetSelection())
-                                       v.cursor.DeleteSelection()
-                                       v.cursor.ResetSelection()
-                                       ret = 2
-                               }
-                       }
+                       v.Cut()
+                       // Rehighlight the entire buffer
+                       v.UpdateLines(v.topline, v.topline+v.height)
                case tcell.KeyCtrlV:
-                       if !clipboard.Unsupported {
-                               if v.cursor.HasSelection() {
-                                       v.cursor.DeleteSelection()
-                                       v.cursor.ResetSelection()
-                               }
-                               clip, _ := clipboard.ReadAll()
-                               v.eh.Insert(v.cursor.loc, clip)
-                               // This is a bit weird... Not sure if there's a better way
-                               for i := 0; i < Count(clip); i++ {
-                                       v.cursor.Right()
-                               }
-                               ret = 2
-                       }
+                       v.Paste()
+                       // Rehighlight the entire buffer
+                       v.UpdateLines(v.topline, v.topline+v.height)
+               case tcell.KeyCtrlA:
+                       v.SelectAll()
+               case tcell.KeyCtrlO:
+                       v.OpenFile()
+                       // Rehighlight the entire buffer
+                       v.UpdateLines(v.topline, v.topline+v.height)
                case tcell.KeyPgUp:
                        v.PageUp()
-                       return 2
                case tcell.KeyPgDn:
                        v.PageDown()
-                       return 2
                case tcell.KeyCtrlU:
                        v.HalfPageUp()
-                       return 2
                case tcell.KeyCtrlD:
                        v.HalfPageDown()
-                       return 2
                case tcell.KeyRune:
+                       // Insert a character
                        if v.cursor.HasSelection() {
                                v.cursor.DeleteSelection()
                                v.cursor.ResetSelection()
                        }
                        v.eh.Insert(v.cursor.loc, string(e.Rune()))
                        v.cursor.Right()
-                       ret = 2
+                       v.UpdateLines(v.cursor.y, v.cursor.y)
                }
        case *tcell.EventMouse:
                x, y := e.Position()
@@ -264,25 +435,8 @@ func (v *View) HandleEvent(event tcell.Event) int {
 
                switch button {
                case tcell.Button1:
-                       if y-v.topline > v.height-1 {
-                               v.ScrollDown(1)
-                               y = v.height + v.topline - 1
-                       }
-                       if y >= len(v.buf.lines) {
-                               y = len(v.buf.lines) - 1
-                       }
-                       if x < 0 {
-                               x = 0
-                       }
-
-                       x = v.cursor.GetCharPosInLine(y, x)
-                       if x > Count(v.buf.lines[y]) {
-                               x = Count(v.buf.lines[y])
-                       }
-                       d := v.cursor.Distance(x, y)
-                       v.cursor.loc += d
-                       v.cursor.x = x
-                       v.cursor.y = y
+                       // Left click
+                       v.MoveToMouseClick(x, y)
 
                        if v.mouseReleased {
                                v.cursor.selectionStart = v.cursor.loc
@@ -291,36 +445,48 @@ func (v *View) HandleEvent(event tcell.Event) int {
                        }
                        v.cursor.selectionEnd = v.cursor.loc
                        v.mouseReleased = false
-                       return 2
                case tcell.ButtonNone:
-                       v.mouseReleased = true
-                       return 0
+                       // Mouse event with no click
+                       if !v.mouseReleased {
+                               // Mouse was just released
+
+                               // Relocating here isn't really necessary because the cursor will
+                               // be in the right place from the last mouse event
+                               // However, if we are running in a terminal that doesn't support mouse motion
+                               // events, this still allows the user to make selections, except only after they
+                               // release the mouse
+                               v.MoveToMouseClick(x, y)
+                               v.cursor.selectionEnd = v.cursor.loc
+                               v.mouseReleased = true
+                       }
+                       // We don't want to relocate because otherwise the view will be relocated
+                       // every time the user moves the cursor
+                       relocate = false
                case tcell.WheelUp:
+                       // Scroll up two lines
                        v.ScrollUp(2)
-                       return 2
+                       // We don't want to relocate if the user is scrolling
+                       relocate = false
                case tcell.WheelDown:
+                       // Scroll down two lines
                        v.ScrollDown(2)
-                       return 2
+                       // We don't want to relocate if the user is scrolling
+                       relocate = false
                }
        }
 
-       cy := v.cursor.y
-       if cy < v.topline {
-               v.topline = cy
-               ret = 2
-       }
-       if cy > v.topline+v.height-1 {
-               v.topline = cy - v.height + 1
-               ret = 2
+       if relocate {
+               v.Relocate()
        }
 
-       return ret
+       // v.matches = Match(v.buf.rules, v.buf, v)
 }
 
-// Display renders the view to the screen
-func (v *View) Display() {
-       var x int
+// DisplayView renders the view to the screen
+func (v *View) DisplayView() {
+       matches := make(SyntaxMatches)
 
+       // The character number of the character in the top left of the screen
        charNum := v.cursor.loc + v.cursor.Distance(0, v.topline)
 
        // Convert the length of buffer to a string, and get the length of the string
@@ -332,6 +498,9 @@ func (v *View) Display() {
        var highlightStyle tcell.Style
 
        for lineN := 0; lineN < v.height; lineN++ {
+               var x int
+               // If the buffer is smaller than the view height
+               // and we went too far, break
                if lineN+v.topline >= len(v.buf.lines) {
                        break
                }
@@ -339,61 +508,88 @@ func (v *View) Display() {
 
                // Write the line number
                lineNumStyle := tcell.StyleDefault
-               if _, ok := colorscheme["line-number"]; ok {
-                       lineNumStyle = colorscheme["line-number"]
+               if style, ok := colorscheme["line-number"]; ok {
+                       lineNumStyle = style
                }
                // Write the spaces before the line number if necessary
                lineNum := strconv.Itoa(lineN + v.topline + 1)
                for i := 0; i < maxLineLength-len(lineNum); i++ {
-                       v.s.SetContent(x, lineN, ' ', nil, lineNumStyle)
+                       screen.SetContent(x, lineN, ' ', nil, lineNumStyle)
                        x++
                }
                // Write the actual line number
                for _, ch := range lineNum {
-                       v.s.SetContent(x, lineN, ch, nil, lineNumStyle)
+                       screen.SetContent(x, lineN, ch, nil, lineNumStyle)
                        x++
                }
                // Write the extra space
-               v.s.SetContent(x, lineN, ' ', nil, lineNumStyle)
+               screen.SetContent(x, lineN, ' ', nil, lineNumStyle)
                x++
 
                // Write the line
                tabchars := 0
                for _, ch := range line {
                        var lineStyle tcell.Style
-                       st, ok := v.matches[charNum]
-                       if ok {
+                       // Does the current character need to be syntax highlighted?
+                       if st, ok := v.matches[charNum]; ok {
+                               highlightStyle = st
+                       } else if st, ok := v.lastMatches[charNum]; ok {
                                highlightStyle = st
                        } else {
                                highlightStyle = tcell.StyleDefault
                        }
+                       matches[charNum] = highlightStyle
 
                        if v.cursor.HasSelection() &&
                                (charNum >= v.cursor.selectionStart && charNum <= v.cursor.selectionEnd ||
                                        charNum <= v.cursor.selectionStart && charNum >= v.cursor.selectionEnd) {
-                               lineStyle = tcell.StyleDefault
-                               lineStyle = lineStyle.Reverse(true)
+
+                               lineStyle = tcell.StyleDefault.Reverse(true)
+
+                               if style, ok := colorscheme["selection"]; ok {
+                                       lineStyle = style
+                               }
                        } else {
                                lineStyle = highlightStyle
                        }
 
                        if ch == '\t' {
-                               v.s.SetContent(x+tabchars, lineN, ' ', nil, lineStyle)
+                               screen.SetContent(x+tabchars, lineN, ' ', nil, lineStyle)
                                for i := 0; i < tabSize-1; i++ {
                                        tabchars++
-                                       v.s.SetContent(x+tabchars, lineN, ' ', nil, lineStyle)
+                                       screen.SetContent(x+tabchars, lineN, ' ', nil, lineStyle)
                                }
                        } else {
-                               v.s.SetContent(x+tabchars, lineN, ch, nil, lineStyle)
+                               screen.SetContent(x+tabchars, lineN, ch, nil, lineStyle)
                        }
                        charNum++
                        x++
                }
-               x = 0
-               st, ok := v.matches[charNum]
-               if ok {
-                       highlightStyle = st
+               // Here we are at a newline
+
+               // The newline may be selected, in which case we should draw the selection style
+               // with a space to represent it
+               if v.cursor.HasSelection() &&
+                       (charNum >= v.cursor.selectionStart && charNum <= v.cursor.selectionEnd ||
+                               charNum <= v.cursor.selectionStart && charNum >= v.cursor.selectionEnd) {
+
+                       selectStyle := tcell.StyleDefault.Reverse(true)
+
+                       if style, ok := colorscheme["selection"]; ok {
+                               selectStyle = style
+                       }
+                       screen.SetContent(x+tabchars, lineN, ' ', nil, selectStyle)
                }
+
                charNum++
        }
+
+       v.lastMatches = matches
+}
+
+// Display renders the view, the cursor, and statusline
+func (v *View) Display() {
+       v.DisplayView()
+       v.cursor.Display()
+       v.sline.Display()
 }