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