]> git.lizzy.rs Git - micro.git/blobdiff - cmd/micro/messenger.go
Code optimisation (#1117)
[micro.git] / cmd / micro / messenger.go
index c7008063e4923c9138a4c3bb3ba482c348529806..b55557af290832cbea0a799e47f2d8fd8ada3ce2 100644 (file)
@@ -3,12 +3,14 @@ package main
 import (
        "bufio"
        "bytes"
+       "encoding/gob"
        "fmt"
        "os"
        "strconv"
-       "strings"
 
+       "github.com/mattn/go-runewidth"
        "github.com/zyedidia/clipboard"
+       "github.com/zyedidia/micro/cmd/micro/shellwords"
        "github.com/zyedidia/tcell"
 )
 
@@ -22,6 +24,7 @@ func TermMessage(msg ...interface{}) {
        screenWasNil := screen == nil
        if !screenWasNil {
                screen.Fini()
+               screen = nil
        }
 
        fmt.Println(msg...)
@@ -44,6 +47,7 @@ func TermError(filename string, lineNum int, err string) {
 // Messenger is an object that makes it easy to send messages to the user
 // and get input from the user
 type Messenger struct {
+       log *Buffer
        // Are we currently prompting the user?
        hasPrompt bool
        // Is there a message to print
@@ -68,37 +72,87 @@ type Messenger struct {
        gutterMessage bool
 }
 
+// AddLog sends a message to the log view
+func (m *Messenger) AddLog(msg ...interface{}) {
+       logMessage := fmt.Sprint(msg...)
+       buffer := m.getBuffer()
+       buffer.insert(buffer.End(), []byte(logMessage+"\n"))
+       buffer.Cursor.Loc = buffer.End()
+       buffer.Cursor.Relocate()
+}
+
+func (m *Messenger) getBuffer() *Buffer {
+       if m.log == nil {
+               m.log = NewBufferFromString("", "")
+               m.log.name = "Log"
+       }
+       return m.log
+}
+
 // Message sends a message to the user
 func (m *Messenger) Message(msg ...interface{}) {
-       buf := new(bytes.Buffer)
-       fmt.Fprint(buf, msg...)
-       m.message = buf.String()
-       m.style = defStyle
+       displayMessage := fmt.Sprint(msg...)
+       // only display a new message if there isn't an active prompt
+       // this is to prevent overwriting an existing prompt to the user
+       if m.hasPrompt == false {
+               // if there is no active prompt then style and display the message as normal
+               m.message = displayMessage
 
-       if _, ok := colorscheme["message"]; ok {
-               m.style = colorscheme["message"]
+               m.style = defStyle
+
+               if _, ok := colorscheme["message"]; ok {
+                       m.style = colorscheme["message"]
+               }
+
+               m.hasMessage = true
        }
-       m.hasMessage = true
+       // add the message to the log regardless of active prompts
+       m.AddLog(displayMessage)
 }
 
 // Error sends an error message to the user
 func (m *Messenger) Error(msg ...interface{}) {
        buf := new(bytes.Buffer)
        fmt.Fprint(buf, msg...)
-       m.message = buf.String()
-       m.style = defStyle.
-               Foreground(tcell.ColorBlack).
-               Background(tcell.ColorMaroon)
 
-       if _, ok := colorscheme["error-message"]; ok {
-               m.style = colorscheme["error-message"]
+       // only display a new message if there isn't an active prompt
+       // this is to prevent overwriting an existing prompt to the user
+       if m.hasPrompt == false {
+               // if there is no active prompt then style and display the message as normal
+               m.message = buf.String()
+               m.style = defStyle.
+                       Foreground(tcell.ColorBlack).
+                       Background(tcell.ColorMaroon)
+
+               if _, ok := colorscheme["error-message"]; ok {
+                       m.style = colorscheme["error-message"]
+               }
+               m.hasMessage = true
        }
+       // add the message to the log regardless of active prompts
+       m.AddLog(buf.String())
+}
+
+func (m *Messenger) PromptText(msg ...interface{}) {
+       displayMessage := fmt.Sprint(msg...)
+       // if there is no active prompt then style and display the message as normal
+       m.message = displayMessage
+
+       m.style = defStyle
+
+       if _, ok := colorscheme["message"]; ok {
+               m.style = colorscheme["message"]
+       }
+
        m.hasMessage = true
+       // add the message to the log regardless of active prompts
+       m.AddLog(displayMessage)
 }
 
 // YesNoPrompt asks the user a yes or no question (waits for y or n) and returns the result
 func (m *Messenger) YesNoPrompt(prompt string) (bool, bool) {
-       m.Message(prompt)
+       m.hasPrompt = true
+       m.PromptText(prompt)
 
        _, h := screen.Size()
        for {
@@ -112,12 +166,20 @@ func (m *Messenger) YesNoPrompt(prompt string) (bool, bool) {
                case *tcell.EventKey:
                        switch e.Key() {
                        case tcell.KeyRune:
-                               if e.Rune() == 'y' {
+                               if e.Rune() == 'y' || e.Rune() == 'Y' {
+                                       m.AddLog("\t--> y")
+                                       m.hasPrompt = false
                                        return true, false
-                               } else if e.Rune() == 'n' {
+                               } else if e.Rune() == 'n' || e.Rune() == 'N' {
+                                       m.AddLog("\t--> n")
+                                       m.hasPrompt = false
                                        return false, false
                                }
                        case tcell.KeyCtrlC, tcell.KeyCtrlQ, tcell.KeyEscape:
+                               m.AddLog("\t--> (cancel)")
+                               m.Clear()
+                               m.Reset()
+                               m.hasPrompt = false
                                return false, true
                        }
                }
@@ -126,7 +188,8 @@ func (m *Messenger) YesNoPrompt(prompt string) (bool, bool) {
 
 // LetterPrompt gives the user a prompt and waits for a one letter response
 func (m *Messenger) LetterPrompt(prompt string, responses ...rune) (rune, bool) {
-       m.Message(prompt)
+       m.hasPrompt = true
+       m.PromptText(prompt)
 
        _, h := screen.Size()
        for {
@@ -142,17 +205,25 @@ func (m *Messenger) LetterPrompt(prompt string, responses ...rune) (rune, bool)
                        case tcell.KeyRune:
                                for _, r := range responses {
                                        if e.Rune() == r {
+                                               m.AddLog("\t--> " + string(r))
+                                               m.Clear()
                                                m.Reset()
+                                               m.hasPrompt = false
                                                return r, false
                                        }
                                }
                        case tcell.KeyCtrlC, tcell.KeyCtrlQ, tcell.KeyEscape:
+                               m.AddLog("\t--> (cancel)")
+                               m.Clear()
+                               m.Reset()
+                               m.hasPrompt = false
                                return ' ', true
                        }
                }
        }
 }
 
+// Completion represents a type of completion
 type Completion int
 
 const (
@@ -161,13 +232,16 @@ const (
        CommandCompletion
        HelpCompletion
        OptionCompletion
+       PluginCmdCompletion
+       PluginNameCompletion
+       OptionValueCompletion
 )
 
 // Prompt sends the user a message and waits for a response to be typed in
 // This function blocks the main loop while waiting for input
-func (m *Messenger) Prompt(prompt, historyType string, completionTypes ...Completion) (string, bool) {
+func (m *Messenger) Prompt(prompt, placeholder, historyType string, completionTypes ...Completion) (string, bool) {
        m.hasPrompt = true
-       m.Message(prompt)
+       m.PromptText(prompt)
        if _, ok := m.history[historyType]; !ok {
                m.history[historyType] = []string{""}
        } else {
@@ -175,7 +249,9 @@ func (m *Messenger) Prompt(prompt, historyType string, completionTypes ...Comple
        }
        m.historyNum = len(m.history[historyType]) - 1
 
-       response, canceled := "", true
+       response, canceled := placeholder, true
+       m.response = response
+       m.cursorx = Count(placeholder)
 
        RedrawAll()
        for m.hasPrompt {
@@ -185,20 +261,34 @@ func (m *Messenger) Prompt(prompt, historyType string, completionTypes ...Comple
                event := <-events
 
                switch e := event.(type) {
+               case *tcell.EventResize:
+                       for _, t := range tabs {
+                               t.Resize()
+                       }
+                       RedrawAll()
                case *tcell.EventKey:
                        switch e.Key() {
                        case tcell.KeyCtrlQ, tcell.KeyCtrlC, tcell.KeyEscape:
                                // Cancel
+                               m.AddLog("\t--> (cancel)")
                                m.hasPrompt = false
                        case tcell.KeyEnter:
                                // User is done entering their response
+                               m.AddLog("\t--> " + m.response)
                                m.hasPrompt = false
                                response, canceled = m.response, false
                                m.history[historyType][len(m.history[historyType])-1] = response
                        case tcell.KeyTab:
-                               args := strings.Split(m.response, " ")
-                               currentArgNum := len(args) - 1
-                               currentArg := args[currentArgNum]
+                               args, err := shellwords.Split(m.response)
+                               if err != nil {
+                                       break
+                               }
+                               currentArg := ""
+                               currentArgNum := 0
+                               if len(args) > 0 {
+                                       currentArgNum = len(args) - 1
+                                       currentArg = args[currentArgNum]
+                               }
                                var completionType Completion
 
                                if completionTypes[0] == CommandCompletion && currentArgNum > 0 {
@@ -222,6 +312,14 @@ func (m *Messenger) Prompt(prompt, historyType string, completionTypes ...Comple
                                        chosen, suggestions = HelpComplete(currentArg)
                                } else if completionType == OptionCompletion {
                                        chosen, suggestions = OptionComplete(currentArg)
+                               } else if completionType == OptionValueCompletion {
+                                       if currentArgNum-1 > 0 {
+                                               chosen, suggestions = OptionValueComplete(args[currentArgNum-1], currentArg)
+                                       }
+                               } else if completionType == PluginCmdCompletion {
+                                       chosen, suggestions = PluginCmdComplete(currentArg)
+                               } else if completionType == PluginNameCompletion {
+                                       chosen, suggestions = PluginNameComplete(currentArg)
                                } else if completionType < NoCompletion {
                                        chosen, suggestions = PluginComplete(completionType, currentArg)
                                }
@@ -230,11 +328,8 @@ func (m *Messenger) Prompt(prompt, historyType string, completionTypes ...Comple
                                        chosen = chosen + CommonSubstring(suggestions...)
                                }
 
-                               if chosen != "" {
-                                       if len(args) > 1 {
-                                               chosen = " " + chosen
-                                       }
-                                       m.response = strings.Join(args[:len(args)-1], " ") + chosen
+                               if len(suggestions) != 0 && chosen != "" {
+                                       m.response = shellwords.Join(append(args[:len(args)-1], chosen)...)
                                        m.cursorx = Count(m.response)
                                }
                        }
@@ -242,56 +337,177 @@ func (m *Messenger) Prompt(prompt, historyType string, completionTypes ...Comple
 
                m.HandleEvent(event, m.history[historyType])
 
-               messenger.Clear()
-               for _, v := range tabs[curTab].views {
+               m.Clear()
+               for _, v := range tabs[curTab].Views {
                        v.Display()
                }
                DisplayTabs()
-               messenger.Display()
+               m.Display()
                if len(suggestions) > 1 {
                        m.DisplaySuggestions(suggestions)
                }
                screen.Show()
        }
 
+       m.Clear()
        m.Reset()
        return response, canceled
 }
 
+// UpHistory fetches the previous item in the history
+func (m *Messenger) UpHistory(history []string) {
+       if m.historyNum > 0 {
+               m.historyNum--
+               m.response = history[m.historyNum]
+               m.cursorx = Count(m.response)
+       }
+}
+
+// DownHistory fetches the next item in the history
+func (m *Messenger) DownHistory(history []string) {
+       if m.historyNum < len(history)-1 {
+               m.historyNum++
+               m.response = history[m.historyNum]
+               m.cursorx = Count(m.response)
+       }
+}
+
+// CursorLeft moves the cursor one character left
+func (m *Messenger) CursorLeft() {
+       if m.cursorx > 0 {
+               m.cursorx--
+       }
+}
+
+// CursorRight moves the cursor one character right
+func (m *Messenger) CursorRight() {
+       if m.cursorx < Count(m.response) {
+               m.cursorx++
+       }
+}
+
+// Start moves the cursor to the start of the line
+func (m *Messenger) Start() {
+       m.cursorx = 0
+}
+
+// End moves the cursor to the end of the line
+func (m *Messenger) End() {
+       m.cursorx = Count(m.response)
+}
+
+// Backspace deletes one character
+func (m *Messenger) Backspace() {
+       if m.cursorx > 0 {
+               m.response = string([]rune(m.response)[:m.cursorx-1]) + string([]rune(m.response)[m.cursorx:])
+               m.cursorx--
+       }
+}
+
+// Paste pastes the clipboard
+func (m *Messenger) Paste() {
+       clip, _ := clipboard.ReadAll("clipboard")
+       m.response = Insert(m.response, m.cursorx, clip)
+       m.cursorx += Count(clip)
+}
+
+// WordLeft moves the cursor one word to the left
+func (m *Messenger) WordLeft() {
+       response := []rune(m.response)
+       m.CursorLeft()
+       if m.cursorx <= 0 {
+               return
+       }
+       for IsWhitespace(response[m.cursorx]) {
+               if m.cursorx <= 0 {
+                       return
+               }
+               m.CursorLeft()
+       }
+       m.CursorLeft()
+       for IsWordChar(string(response[m.cursorx])) {
+               if m.cursorx <= 0 {
+                       return
+               }
+               m.CursorLeft()
+       }
+       m.CursorRight()
+}
+
+// WordRight moves the cursor one word to the right
+func (m *Messenger) WordRight() {
+       response := []rune(m.response)
+       if m.cursorx >= len(response) {
+               return
+       }
+       for IsWhitespace(response[m.cursorx]) {
+               m.CursorRight()
+               if m.cursorx >= len(response) {
+                       m.CursorRight()
+                       return
+               }
+       }
+       m.CursorRight()
+       if m.cursorx >= len(response) {
+               return
+       }
+       for IsWordChar(string(response[m.cursorx])) {
+               m.CursorRight()
+               if m.cursorx >= len(response) {
+                       return
+               }
+       }
+}
+
+// DeleteWordLeft deletes one word to the left
+func (m *Messenger) DeleteWordLeft() {
+       m.WordLeft()
+       m.response = string([]rune(m.response)[:m.cursorx])
+}
+
 // HandleEvent handles an event for the prompter
 func (m *Messenger) HandleEvent(event tcell.Event, history []string) {
        switch e := event.(type) {
        case *tcell.EventKey:
                switch e.Key() {
+               case tcell.KeyCtrlA:
+                       m.Start()
+               case tcell.KeyCtrlE:
+                       m.End()
                case tcell.KeyUp:
-                       if m.historyNum > 0 {
-                               m.historyNum--
-                               m.response = history[m.historyNum]
-                               m.cursorx = Count(m.response)
-                       }
+                       m.UpHistory(history)
                case tcell.KeyDown:
-                       if m.historyNum < len(history)-1 {
-                               m.historyNum++
-                               m.response = history[m.historyNum]
-                               m.cursorx = Count(m.response)
-                       }
+                       m.DownHistory(history)
                case tcell.KeyLeft:
-                       if m.cursorx > 0 {
-                               m.cursorx--
+                       if e.Modifiers() == tcell.ModCtrl {
+                               m.Start()
+                       } else if e.Modifiers() == tcell.ModAlt || e.Modifiers() == tcell.ModMeta {
+                               m.WordLeft()
+                       } else {
+                               m.CursorLeft()
                        }
                case tcell.KeyRight:
-                       if m.cursorx < Count(m.response) {
-                               m.cursorx++
+                       if e.Modifiers() == tcell.ModCtrl {
+                               m.End()
+                       } else if e.Modifiers() == tcell.ModAlt || e.Modifiers() == tcell.ModMeta {
+                               m.WordRight()
+                       } else {
+                               m.CursorRight()
                        }
                case tcell.KeyBackspace2, tcell.KeyBackspace:
-                       if m.cursorx > 0 {
-                               m.response = string([]rune(m.response)[:m.cursorx-1]) + string([]rune(m.response)[m.cursorx:])
-                               m.cursorx--
+                       if e.Modifiers() == tcell.ModCtrl || e.Modifiers() == tcell.ModAlt || e.Modifiers() == tcell.ModMeta {
+                               m.DeleteWordLeft()
+                       } else {
+                               m.Backspace()
                        }
+               case tcell.KeyCtrlW:
+                       m.DeleteWordLeft()
                case tcell.KeyCtrlV:
-                       clip, _ := clipboard.ReadAll()
-                       m.response = Insert(m.response, m.cursorx, clip)
-                       m.cursorx += Count(clip)
+                       m.Paste()
+               case tcell.KeyCtrlF:
+                       m.WordRight()
+               case tcell.KeyCtrlB:
+                       m.WordLeft()
                case tcell.KeyRune:
                        m.response = Insert(m.response, m.cursorx, string(e.Rune()))
                        m.cursorx++
@@ -302,6 +518,23 @@ func (m *Messenger) HandleEvent(event tcell.Event, history []string) {
                clip := e.Text()
                m.response = Insert(m.response, m.cursorx, clip)
                m.cursorx += Count(clip)
+       case *tcell.EventMouse:
+               x, y := e.Position()
+               x -= Count(m.message)
+               button := e.Buttons()
+               _, screenH := screen.Size()
+
+               if y == screenH-1 {
+                       switch button {
+                       case tcell.Button1:
+                               m.cursorx = x
+                               if m.cursorx < 0 {
+                                       m.cursorx = 0
+                               } else if m.cursorx > Count(m.response) {
+                                       m.cursorx = Count(m.response)
+                               }
+                       }
+               }
        }
 }
 
@@ -349,20 +582,75 @@ func (m *Messenger) DisplaySuggestions(suggestions []string) {
 func (m *Messenger) Display() {
        _, h := screen.Size()
        if m.hasMessage {
-               if !m.hasPrompt && !globalSettings["infobar"].(bool) {
-                       return
-               }
-               runes := []rune(m.message + m.response)
-               for x := 0; x < len(runes); x++ {
-                       screen.SetContent(x, h-1, runes[x], nil, m.style)
+               if m.hasPrompt || globalSettings["infobar"].(bool) {
+                       runes := []rune(m.message + m.response)
+                       posx := 0
+                       for x := 0; x < len(runes); x++ {
+                               screen.SetContent(posx, h-1, runes[x], nil, m.style)
+                               posx += runewidth.RuneWidth(runes[x])
+                       }
                }
        }
+
        if m.hasPrompt {
                screen.ShowCursor(Count(m.message)+m.cursorx, h-1)
                screen.Show()
        }
 }
 
+// LoadHistory attempts to load user history from configDir/buffers/history
+// into the history map
+// The savehistory option must be on
+func (m *Messenger) LoadHistory() {
+       if GetGlobalOption("savehistory").(bool) {
+               file, err := os.Open(configDir + "/buffers/history")
+               defer file.Close()
+               var decodedMap map[string][]string
+               if err == nil {
+                       decoder := gob.NewDecoder(file)
+                       err = decoder.Decode(&decodedMap)
+
+                       if err != nil {
+                               m.Error("Error loading history:", err)
+                               return
+                       }
+               }
+
+               if decodedMap != nil {
+                       m.history = decodedMap
+               } else {
+                       m.history = make(map[string][]string)
+               }
+       } else {
+               m.history = make(map[string][]string)
+       }
+}
+
+// SaveHistory saves the user's command history to configDir/buffers/history
+// only if the savehistory option is on
+func (m *Messenger) SaveHistory() {
+       if GetGlobalOption("savehistory").(bool) {
+               // Don't save history past 100
+               for k, v := range m.history {
+                       if len(v) > 100 {
+                               m.history[k] = v[len(m.history[k])-100:]
+                       }
+               }
+
+               file, err := os.Create(configDir + "/buffers/history")
+               defer file.Close()
+               if err == nil {
+                       encoder := gob.NewEncoder(file)
+
+                       err = encoder.Encode(m.history)
+                       if err != nil {
+                               m.Error("Error saving history:", err)
+                               return
+                       }
+               }
+       }
+}
+
 // A GutterMessage is a message displayed on the side of the editor
 type GutterMessage struct {
        lineNum int