X-Git-Url: https://git.lizzy.rs/?a=blobdiff_plain;f=cmd%2Fmicro%2Fmessenger.go;h=b55557af290832cbea0a799e47f2d8fd8ada3ce2;hb=41a24e61d6b9017dbe010ae36295cb3c1dd701fc;hp=b53687b1f199e09af5205eb39a811436881c27eb;hpb=a4ac9f2b7b67bc728968dc86b2d9458b33c7e8b1;p=micro.git diff --git a/cmd/micro/messenger.go b/cmd/micro/messenger.go index b53687b1..b55557af 100644 --- a/cmd/micro/messenger.go +++ b/cmd/micro/messenger.go @@ -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,38 +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.hasPrompt = true - m.Message(prompt) + m.PromptText(prompt) _, h := screen.Size() for { @@ -113,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 } } @@ -128,7 +189,7 @@ 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.hasPrompt = true - m.Message(prompt) + m.PromptText(prompt) _, h := screen.Size() for { @@ -144,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 ( @@ -163,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 { @@ -177,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 { @@ -187,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 { @@ -224,17 +312,24 @@ 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) } if len(suggestions) > 1 { 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) + } + } + } } } @@ -351,8 +584,10 @@ func (m *Messenger) Display() { if m.hasMessage { if m.hasPrompt || globalSettings["infobar"].(bool) { runes := []rune(m.message + m.response) + posx := 0 for x := 0; x < len(runes); x++ { - screen.SetContent(x, h-1, runes[x], nil, m.style) + screen.SetContent(posx, h-1, runes[x], nil, m.style) + posx += runewidth.RuneWidth(runes[x]) } } } @@ -363,6 +598,59 @@ func (m *Messenger) Display() { } } +// 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