]> git.lizzy.rs Git - micro.git/blob - cmd/micro/messenger.go
27800dd4e9bad264e68b0d73c226dffa77e9853e
[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         }
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         // 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 // Message sends a message to the user
72 func (m *Messenger) Message(msg ...interface{}) {
73         buf := new(bytes.Buffer)
74         fmt.Fprint(buf, msg...)
75         m.message = buf.String()
76         m.style = defStyle
77
78         if _, ok := colorscheme["message"]; ok {
79                 m.style = colorscheme["message"]
80         }
81         m.hasMessage = true
82 }
83
84 // Error sends an error message to the user
85 func (m *Messenger) Error(msg ...interface{}) {
86         buf := new(bytes.Buffer)
87         fmt.Fprint(buf, msg...)
88         m.message = buf.String()
89         m.style = defStyle.
90                 Foreground(tcell.ColorBlack).
91                 Background(tcell.ColorMaroon)
92
93         if _, ok := colorscheme["error-message"]; ok {
94                 m.style = colorscheme["error-message"]
95         }
96         m.hasMessage = true
97 }
98
99 // YesNoPrompt asks the user a yes or no question (waits for y or n) and returns the result
100 func (m *Messenger) YesNoPrompt(prompt string) (bool, bool) {
101         m.hasPrompt = true
102         m.Message(prompt)
103
104         _, h := screen.Size()
105         for {
106                 m.Clear()
107                 m.Display()
108                 screen.ShowCursor(Count(m.message), h-1)
109                 screen.Show()
110                 event := <-events
111
112                 switch e := event.(type) {
113                 case *tcell.EventKey:
114                         switch e.Key() {
115                         case tcell.KeyRune:
116                                 if e.Rune() == 'y' {
117                                         m.hasPrompt = false
118                                         return true, false
119                                 } else if e.Rune() == 'n' {
120                                         m.hasPrompt = false
121                                         return false, false
122                                 }
123                         case tcell.KeyCtrlC, tcell.KeyCtrlQ, tcell.KeyEscape:
124                                 m.hasPrompt = false
125                                 return false, true
126                         }
127                 }
128         }
129 }
130
131 // LetterPrompt gives the user a prompt and waits for a one letter response
132 func (m *Messenger) LetterPrompt(prompt string, responses ...rune) (rune, bool) {
133         m.hasPrompt = true
134         m.Message(prompt)
135
136         _, h := screen.Size()
137         for {
138                 m.Clear()
139                 m.Display()
140                 screen.ShowCursor(Count(m.message), h-1)
141                 screen.Show()
142                 event := <-events
143
144                 switch e := event.(type) {
145                 case *tcell.EventKey:
146                         switch e.Key() {
147                         case tcell.KeyRune:
148                                 for _, r := range responses {
149                                         if e.Rune() == r {
150                                                 m.Clear()
151                                                 m.Reset()
152                                                 m.hasPrompt = false
153                                                 return r, false
154                                         }
155                                 }
156                         case tcell.KeyCtrlC, tcell.KeyCtrlQ, tcell.KeyEscape:
157                                 m.Clear()
158                                 m.Reset()
159                                 m.hasPrompt = false
160                                 return ' ', true
161                         }
162                 }
163         }
164 }
165
166 type Completion int
167
168 const (
169         NoCompletion Completion = iota
170         FileCompletion
171         CommandCompletion
172         HelpCompletion
173         OptionCompletion
174 )
175
176 // Prompt sends the user a message and waits for a response to be typed in
177 // This function blocks the main loop while waiting for input
178 func (m *Messenger) Prompt(prompt, historyType string, completionTypes ...Completion) (string, bool) {
179         m.hasPrompt = true
180         m.Message(prompt)
181         if _, ok := m.history[historyType]; !ok {
182                 m.history[historyType] = []string{""}
183         } else {
184                 m.history[historyType] = append(m.history[historyType], "")
185         }
186         m.historyNum = len(m.history[historyType]) - 1
187
188         response, canceled := "", true
189
190         RedrawAll()
191         for m.hasPrompt {
192                 var suggestions []string
193                 m.Clear()
194
195                 event := <-events
196
197                 switch e := event.(type) {
198                 case *tcell.EventKey:
199                         switch e.Key() {
200                         case tcell.KeyCtrlQ, tcell.KeyCtrlC, tcell.KeyEscape:
201                                 // Cancel
202                                 m.hasPrompt = false
203                         case tcell.KeyEnter:
204                                 // User is done entering their response
205                                 m.hasPrompt = false
206                                 response, canceled = m.response, false
207                                 m.history[historyType][len(m.history[historyType])-1] = response
208                         case tcell.KeyTab:
209                                 args := strings.Split(m.response, " ")
210                                 currentArgNum := len(args) - 1
211                                 currentArg := args[currentArgNum]
212                                 var completionType Completion
213
214                                 if completionTypes[0] == CommandCompletion && currentArgNum > 0 {
215                                         if command, ok := commands[args[0]]; ok {
216                                                 completionTypes = append([]Completion{CommandCompletion}, command.completions...)
217                                         }
218                                 }
219
220                                 if currentArgNum >= len(completionTypes) {
221                                         completionType = completionTypes[len(completionTypes)-1]
222                                 } else {
223                                         completionType = completionTypes[currentArgNum]
224                                 }
225
226                                 var chosen string
227                                 if completionType == FileCompletion {
228                                         chosen, suggestions = FileComplete(currentArg)
229                                 } else if completionType == CommandCompletion {
230                                         chosen, suggestions = CommandComplete(currentArg)
231                                 } else if completionType == HelpCompletion {
232                                         chosen, suggestions = HelpComplete(currentArg)
233                                 } else if completionType == OptionCompletion {
234                                         chosen, suggestions = OptionComplete(currentArg)
235                                 } else if completionType < NoCompletion {
236                                         chosen, suggestions = PluginComplete(completionType, currentArg)
237                                 }
238
239                                 if len(suggestions) > 1 {
240                                         chosen = chosen + CommonSubstring(suggestions...)
241                                 }
242
243                                 if chosen != "" {
244                                         if len(args) > 1 {
245                                                 chosen = " " + chosen
246                                         }
247                                         m.response = strings.Join(args[:len(args)-1], " ") + chosen
248                                         m.cursorx = Count(m.response)
249                                 }
250                         }
251                 }
252
253                 m.HandleEvent(event, m.history[historyType])
254
255                 m.Clear()
256                 for _, v := range tabs[curTab].views {
257                         v.Display()
258                 }
259                 DisplayTabs()
260                 m.Display()
261                 if len(suggestions) > 1 {
262                         m.DisplaySuggestions(suggestions)
263                 }
264                 screen.Show()
265         }
266
267         m.Clear()
268         m.Reset()
269         return response, canceled
270 }
271
272 // HandleEvent handles an event for the prompter
273 func (m *Messenger) HandleEvent(event tcell.Event, history []string) {
274         switch e := event.(type) {
275         case *tcell.EventKey:
276                 switch e.Key() {
277                 case tcell.KeyUp:
278                         if m.historyNum > 0 {
279                                 m.historyNum--
280                                 m.response = history[m.historyNum]
281                                 m.cursorx = Count(m.response)
282                         }
283                 case tcell.KeyDown:
284                         if m.historyNum < len(history)-1 {
285                                 m.historyNum++
286                                 m.response = history[m.historyNum]
287                                 m.cursorx = Count(m.response)
288                         }
289                 case tcell.KeyLeft:
290                         if m.cursorx > 0 {
291                                 m.cursorx--
292                         }
293                 case tcell.KeyRight:
294                         if m.cursorx < Count(m.response) {
295                                 m.cursorx++
296                         }
297                 case tcell.KeyBackspace2, tcell.KeyBackspace:
298                         if m.cursorx > 0 {
299                                 m.response = string([]rune(m.response)[:m.cursorx-1]) + string([]rune(m.response)[m.cursorx:])
300                                 m.cursorx--
301                         }
302                 case tcell.KeyCtrlV:
303                         clip, _ := clipboard.ReadAll()
304                         m.response = Insert(m.response, m.cursorx, clip)
305                         m.cursorx += Count(clip)
306                 case tcell.KeyRune:
307                         m.response = Insert(m.response, m.cursorx, string(e.Rune()))
308                         m.cursorx++
309                 }
310                 history[m.historyNum] = m.response
311
312         case *tcell.EventPaste:
313                 clip := e.Text()
314                 m.response = Insert(m.response, m.cursorx, clip)
315                 m.cursorx += Count(clip)
316         }
317 }
318
319 // Reset resets the messenger's cursor, message and response
320 func (m *Messenger) Reset() {
321         m.cursorx = 0
322         m.message = ""
323         m.response = ""
324 }
325
326 // Clear clears the line at the bottom of the editor
327 func (m *Messenger) Clear() {
328         w, h := screen.Size()
329         for x := 0; x < w; x++ {
330                 screen.SetContent(x, h-1, ' ', nil, defStyle)
331         }
332 }
333
334 func (m *Messenger) DisplaySuggestions(suggestions []string) {
335         w, screenH := screen.Size()
336
337         y := screenH - 2
338
339         statusLineStyle := defStyle.Reverse(true)
340         if style, ok := colorscheme["statusline"]; ok {
341                 statusLineStyle = style
342         }
343
344         for x := 0; x < w; x++ {
345                 screen.SetContent(x, y, ' ', nil, statusLineStyle)
346         }
347
348         x := 0
349         for _, suggestion := range suggestions {
350                 for _, c := range suggestion {
351                         screen.SetContent(x, y, c, nil, statusLineStyle)
352                         x++
353                 }
354                 screen.SetContent(x, y, ' ', nil, statusLineStyle)
355                 x++
356         }
357 }
358
359 // Display displays messages or prompts
360 func (m *Messenger) Display() {
361         _, h := screen.Size()
362         if m.hasMessage {
363                 if m.hasPrompt || globalSettings["infobar"].(bool) {
364                         runes := []rune(m.message + m.response)
365                         for x := 0; x < len(runes); x++ {
366                                 screen.SetContent(x, h-1, runes[x], nil, m.style)
367                         }
368                 }
369         }
370
371         if m.hasPrompt {
372                 screen.ShowCursor(Count(m.message)+m.cursorx, h-1)
373                 screen.Show()
374         }
375 }
376
377 // A GutterMessage is a message displayed on the side of the editor
378 type GutterMessage struct {
379         lineNum int
380         msg     string
381         kind    int
382 }
383
384 // These are the different types of messages
385 const (
386         // GutterInfo represents a simple info message
387         GutterInfo = iota
388         // GutterWarning represents a compiler warning
389         GutterWarning
390         // GutterError represents a compiler error
391         GutterError
392 )