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