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