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