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