]> git.lizzy.rs Git - micro.git/blob - cmd/micro/messenger.go
Fix some lint problems
[micro.git] / cmd / micro / messenger.go
1 package main
2
3 import (
4         "bufio"
5         "bytes"
6         "fmt"
7         "os"
8         "strconv"
9
10         "github.com/mattn/go-runewidth"
11         "github.com/zyedidia/clipboard"
12         "github.com/zyedidia/tcell"
13 )
14
15 // TermMessage sends a message to the user in the terminal. This usually occurs before
16 // micro has been fully initialized -- ie if there is an error in the syntax highlighting
17 // regular expressions
18 // The function must be called when the screen is not initialized
19 // This will write the message, and wait for the user
20 // to press and key to continue
21 func TermMessage(msg ...interface{}) {
22         screenWasNil := screen == nil
23         if !screenWasNil {
24                 screen.Fini()
25                 screen = nil
26         }
27
28         fmt.Println(msg...)
29         fmt.Print("\nPress enter to continue")
30
31         reader := bufio.NewReader(os.Stdin)
32         reader.ReadString('\n')
33
34         if !screenWasNil {
35                 InitScreen()
36         }
37 }
38
39 // TermError sends an error to the user in the terminal. Like TermMessage except formatted
40 // as an error
41 func TermError(filename string, lineNum int, err string) {
42         TermMessage(filename + ", " + strconv.Itoa(lineNum) + ": " + err)
43 }
44
45 // Messenger is an object that makes it easy to send messages to the user
46 // and get input from the user
47 type Messenger struct {
48         log *Buffer
49         // Are we currently prompting the user?
50         hasPrompt bool
51         // Is there a message to print
52         hasMessage bool
53
54         // Message to print
55         message string
56         // The user's response to a prompt
57         response string
58         // style to use when drawing the message
59         style tcell.Style
60
61         // We have to keep track of the cursor for prompting
62         cursorx int
63
64         // This map stores the history for all the different kinds of uses Prompt has
65         // It's a map of history type -> history array
66         history    map[string][]string
67         historyNum int
68
69         // Is the current message a message from the gutter
70         gutterMessage bool
71 }
72
73 func (m *Messenger) AddLog(msg string) {
74         buffer := m.getBuffer()
75         buffer.insert(buffer.End(), []byte(msg+"\n"))
76         buffer.Cursor.Loc = buffer.End()
77         buffer.Cursor.Relocate()
78 }
79
80 func (m *Messenger) getBuffer() *Buffer {
81         if m.log == nil {
82                 m.log = NewBufferFromString("", "")
83                 m.log.name = "Log"
84         }
85         return m.log
86 }
87
88 // Message sends a message to the user
89 func (m *Messenger) Message(msg ...interface{}) {
90         displayMessage := fmt.Sprint(msg...)
91         // only display a new message if there isn't an active prompt
92         // this is to prevent overwriting an existing prompt to the user
93         if m.hasPrompt == false {
94                 // if there is no active prompt then style and display the message as normal
95                 m.message = displayMessage
96
97                 m.style = defStyle
98
99                 if _, ok := colorscheme["message"]; ok {
100                         m.style = colorscheme["message"]
101                 }
102
103                 m.hasMessage = true
104         }
105         // add the message to the log regardless of active prompts
106         m.AddLog(displayMessage)
107 }
108
109 // Error sends an error message to the user
110 func (m *Messenger) Error(msg ...interface{}) {
111         buf := new(bytes.Buffer)
112         fmt.Fprint(buf, msg...)
113
114         // only display a new message if there isn't an active prompt
115         // this is to prevent overwriting an existing prompt to the user
116         if m.hasPrompt == false {
117                 // if there is no active prompt then style and display the message as normal
118                 m.message = buf.String()
119                 m.style = defStyle.
120                         Foreground(tcell.ColorBlack).
121                         Background(tcell.ColorMaroon)
122
123                 if _, ok := colorscheme["error-message"]; ok {
124                         m.style = colorscheme["error-message"]
125                 }
126                 m.hasMessage = true
127         }
128         // add the message to the log regardless of active prompts
129         m.AddLog(buf.String())
130 }
131
132 func (m *Messenger) PromptText(msg ...interface{}) {
133         displayMessage := fmt.Sprint(msg...)
134         // if there is no active prompt then style and display the message as normal
135         m.message = displayMessage
136
137         m.style = defStyle
138
139         if _, ok := colorscheme["message"]; ok {
140                 m.style = colorscheme["message"]
141         }
142
143         m.hasMessage = true
144         // add the message to the log regardless of active prompts
145         m.AddLog(displayMessage)
146 }
147
148 // YesNoPrompt asks the user a yes or no question (waits for y or n) and returns the result
149 func (m *Messenger) YesNoPrompt(prompt string) (bool, bool) {
150         m.hasPrompt = true
151         m.PromptText(prompt)
152
153         _, h := screen.Size()
154         for {
155                 m.Clear()
156                 m.Display()
157                 screen.ShowCursor(Count(m.message), h-1)
158                 screen.Show()
159                 event := <-events
160
161                 switch e := event.(type) {
162                 case *tcell.EventKey:
163                         switch e.Key() {
164                         case tcell.KeyRune:
165                                 if e.Rune() == 'y' || e.Rune() == 'Y' {
166                                         m.AddLog("\t--> y")
167                                         m.hasPrompt = false
168                                         return true, false
169                                 } else if e.Rune() == 'n' || e.Rune() == 'N' {
170                                         m.AddLog("\t--> n")
171                                         m.hasPrompt = false
172                                         return false, false
173                                 }
174                         case tcell.KeyCtrlC, tcell.KeyCtrlQ, tcell.KeyEscape:
175                                 m.AddLog("\t--> (cancel)")
176                                 m.Clear()
177                                 m.Reset()
178                                 m.hasPrompt = false
179                                 return false, true
180                         }
181                 }
182         }
183 }
184
185 // LetterPrompt gives the user a prompt and waits for a one letter response
186 func (m *Messenger) LetterPrompt(prompt string, responses ...rune) (rune, bool) {
187         m.hasPrompt = true
188         m.PromptText(prompt)
189
190         _, h := screen.Size()
191         for {
192                 m.Clear()
193                 m.Display()
194                 screen.ShowCursor(Count(m.message), h-1)
195                 screen.Show()
196                 event := <-events
197
198                 switch e := event.(type) {
199                 case *tcell.EventKey:
200                         switch e.Key() {
201                         case tcell.KeyRune:
202                                 for _, r := range responses {
203                                         if e.Rune() == r {
204                                                 m.AddLog("\t--> " + string(r))
205                                                 m.Clear()
206                                                 m.Reset()
207                                                 m.hasPrompt = false
208                                                 return r, false
209                                         }
210                                 }
211                         case tcell.KeyCtrlC, tcell.KeyCtrlQ, tcell.KeyEscape:
212                                 m.AddLog("\t--> (cancel)")
213                                 m.Clear()
214                                 m.Reset()
215                                 m.hasPrompt = false
216                                 return ' ', true
217                         }
218                 }
219         }
220 }
221
222 type Completion int
223
224 const (
225         NoCompletion Completion = iota
226         FileCompletion
227         CommandCompletion
228         HelpCompletion
229         OptionCompletion
230         PluginCmdCompletion
231         PluginNameCompletion
232 )
233
234 // Prompt sends the user a message and waits for a response to be typed in
235 // This function blocks the main loop while waiting for input
236 func (m *Messenger) Prompt(prompt, placeholder, historyType string, completionTypes ...Completion) (string, bool) {
237         m.hasPrompt = true
238         m.PromptText(prompt)
239         if _, ok := m.history[historyType]; !ok {
240                 m.history[historyType] = []string{""}
241         } else {
242                 m.history[historyType] = append(m.history[historyType], "")
243         }
244         m.historyNum = len(m.history[historyType]) - 1
245
246         response, canceled := placeholder, true
247         m.response = response
248         m.cursorx = Count(placeholder)
249
250         RedrawAll()
251         for m.hasPrompt {
252                 var suggestions []string
253                 m.Clear()
254
255                 event := <-events
256
257                 switch e := event.(type) {
258                 case *tcell.EventKey:
259                         switch e.Key() {
260                         case tcell.KeyCtrlQ, tcell.KeyCtrlC, tcell.KeyEscape:
261                                 // Cancel
262                                 m.AddLog("\t--> (cancel)")
263                                 m.hasPrompt = false
264                         case tcell.KeyEnter:
265                                 // User is done entering their response
266                                 m.AddLog("\t--> " + m.response)
267                                 m.hasPrompt = false
268                                 response, canceled = m.response, false
269                                 m.history[historyType][len(m.history[historyType])-1] = response
270                         case tcell.KeyTab:
271                                 args := SplitCommandArgs(m.response)
272                                 currentArgNum := len(args) - 1
273                                 currentArg := args[currentArgNum]
274                                 var completionType Completion
275
276                                 if completionTypes[0] == CommandCompletion && currentArgNum > 0 {
277                                         if command, ok := commands[args[0]]; ok {
278                                                 completionTypes = append([]Completion{CommandCompletion}, command.completions...)
279                                         }
280                                 }
281
282                                 if currentArgNum >= len(completionTypes) {
283                                         completionType = completionTypes[len(completionTypes)-1]
284                                 } else {
285                                         completionType = completionTypes[currentArgNum]
286                                 }
287
288                                 var chosen string
289                                 if completionType == FileCompletion {
290                                         chosen, suggestions = FileComplete(currentArg)
291                                 } else if completionType == CommandCompletion {
292                                         chosen, suggestions = CommandComplete(currentArg)
293                                 } else if completionType == HelpCompletion {
294                                         chosen, suggestions = HelpComplete(currentArg)
295                                 } else if completionType == OptionCompletion {
296                                         chosen, suggestions = OptionComplete(currentArg)
297                                 } else if completionType == PluginCmdCompletion {
298                                         chosen, suggestions = PluginCmdComplete(currentArg)
299                                 } else if completionType == PluginNameCompletion {
300                                         chosen, suggestions = PluginNameComplete(currentArg)
301                                 } else if completionType < NoCompletion {
302                                         chosen, suggestions = PluginComplete(completionType, currentArg)
303                                 }
304
305                                 if len(suggestions) > 1 {
306                                         chosen = chosen + CommonSubstring(suggestions...)
307                                 }
308
309                                 if chosen != "" {
310                                         m.response = JoinCommandArgs(append(args[:len(args)-1], chosen)...)
311                                         m.cursorx = Count(m.response)
312                                 }
313                         }
314                 }
315
316                 m.HandleEvent(event, m.history[historyType])
317
318                 m.Clear()
319                 for _, v := range tabs[curTab].views {
320                         v.Display()
321                 }
322                 DisplayTabs()
323                 m.Display()
324                 if len(suggestions) > 1 {
325                         m.DisplaySuggestions(suggestions)
326                 }
327                 screen.Show()
328         }
329
330         m.Clear()
331         m.Reset()
332         return response, canceled
333 }
334
335 // HandleEvent handles an event for the prompter
336 func (m *Messenger) HandleEvent(event tcell.Event, history []string) {
337         switch e := event.(type) {
338         case *tcell.EventKey:
339                 if e.Key() != tcell.KeyRune || e.Modifiers() != 0 {
340                         for key, actions := range bindings {
341                                 if e.Key() == key.keyCode {
342                                         if e.Key() == tcell.KeyRune {
343                                                 if e.Rune() != key.r {
344                                                         continue
345                                                 }
346                                         }
347                                         if e.Modifiers() == key.modifiers {
348                                                 for _, action := range actions {
349                                                         funcName := FuncName(action)
350                                                         switch funcName {
351                                                         case "main.(*View).CursorUp":
352                                                                 if m.historyNum > 0 {
353                                                                         m.historyNum--
354                                                                         m.response = history[m.historyNum]
355                                                                         m.cursorx = Count(m.response)
356                                                                 }
357                                                         case "main.(*View).CursorDown":
358                                                                 if m.historyNum < len(history)-1 {
359                                                                         m.historyNum++
360                                                                         m.response = history[m.historyNum]
361                                                                         m.cursorx = Count(m.response)
362                                                                 }
363                                                         case "main.(*View).CursorLeft":
364                                                                 if m.cursorx > 0 {
365                                                                         m.cursorx--
366                                                                 }
367                                                         case "main.(*View).CursorRight":
368                                                                 if m.cursorx < Count(m.response) {
369                                                                         m.cursorx++
370                                                                 }
371                                                         case "main.(*View).CursorStart", "main.(*View).StartOfLine":
372                                                                 m.cursorx = 0
373                                                         case "main.(*View).CursorEnd", "main.(*View).EndOfLine":
374                                                                 m.cursorx = Count(m.response)
375                                                         case "main.(*View).Backspace":
376                                                                 if m.cursorx > 0 {
377                                                                         m.response = string([]rune(m.response)[:m.cursorx-1]) + string([]rune(m.response)[m.cursorx:])
378                                                                         m.cursorx--
379                                                                 }
380                                                         case "main.(*View).Paste":
381                                                                 clip, _ := clipboard.ReadAll("clipboard")
382                                                                 m.response = Insert(m.response, m.cursorx, clip)
383                                                                 m.cursorx += Count(clip)
384                                                         }
385                                                 }
386                                         }
387                                 }
388                         }
389                 }
390                 switch e.Key() {
391                 case tcell.KeyRune:
392                         m.response = Insert(m.response, m.cursorx, string(e.Rune()))
393                         m.cursorx++
394                 }
395                 history[m.historyNum] = m.response
396
397         case *tcell.EventPaste:
398                 clip := e.Text()
399                 m.response = Insert(m.response, m.cursorx, clip)
400                 m.cursorx += Count(clip)
401         case *tcell.EventMouse:
402                 x, y := e.Position()
403                 x -= Count(m.message)
404                 button := e.Buttons()
405                 _, screenH := screen.Size()
406
407                 if y == screenH-1 {
408                         switch button {
409                         case tcell.Button1:
410                                 m.cursorx = x
411                                 if m.cursorx < 0 {
412                                         m.cursorx = 0
413                                 } else if m.cursorx > Count(m.response) {
414                                         m.cursorx = Count(m.response)
415                                 }
416                         }
417                 }
418         }
419 }
420
421 // Reset resets the messenger's cursor, message and response
422 func (m *Messenger) Reset() {
423         m.cursorx = 0
424         m.message = ""
425         m.response = ""
426 }
427
428 // Clear clears the line at the bottom of the editor
429 func (m *Messenger) Clear() {
430         w, h := screen.Size()
431         for x := 0; x < w; x++ {
432                 screen.SetContent(x, h-1, ' ', nil, defStyle)
433         }
434 }
435
436 func (m *Messenger) DisplaySuggestions(suggestions []string) {
437         w, screenH := screen.Size()
438
439         y := screenH - 2
440
441         statusLineStyle := defStyle.Reverse(true)
442         if style, ok := colorscheme["statusline"]; ok {
443                 statusLineStyle = style
444         }
445
446         for x := 0; x < w; x++ {
447                 screen.SetContent(x, y, ' ', nil, statusLineStyle)
448         }
449
450         x := 0
451         for _, suggestion := range suggestions {
452                 for _, c := range suggestion {
453                         screen.SetContent(x, y, c, nil, statusLineStyle)
454                         x++
455                 }
456                 screen.SetContent(x, y, ' ', nil, statusLineStyle)
457                 x++
458         }
459 }
460
461 // Display displays messages or prompts
462 func (m *Messenger) Display() {
463         _, h := screen.Size()
464         if m.hasMessage {
465                 if m.hasPrompt || globalSettings["infobar"].(bool) {
466                         runes := []rune(m.message + m.response)
467                         posx := 0
468                         for x := 0; x < len(runes); x++ {
469                                 screen.SetContent(posx, h-1, runes[x], nil, m.style)
470                                 posx += runewidth.RuneWidth(runes[x])
471                         }
472                 }
473         }
474
475         if m.hasPrompt {
476                 screen.ShowCursor(Count(m.message)+m.cursorx, h-1)
477                 screen.Show()
478         }
479 }
480
481 // A GutterMessage is a message displayed on the side of the editor
482 type GutterMessage struct {
483         lineNum int
484         msg     string
485         kind    int
486 }
487
488 // These are the different types of messages
489 const (
490         // GutterInfo represents a simple info message
491         GutterInfo = iota
492         // GutterWarning represents a compiler warning
493         GutterWarning
494         // GutterError represents a compiler error
495         GutterError
496 )