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