]> git.lizzy.rs Git - micro.git/blob - cmd/micro/messenger.go
a9535b35ca2277f17512fa3a3c85ef18be3052dd
[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         messenger.AddLog(fmt.Sprint(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 )
196
197 // Prompt sends the user a message and waits for a response to be typed in
198 // This function blocks the main loop while waiting for input
199 func (m *Messenger) Prompt(prompt, historyType string, completionTypes ...Completion) (string, bool) {
200         m.hasPrompt = true
201         m.Message(prompt)
202         if _, ok := m.history[historyType]; !ok {
203                 m.history[historyType] = []string{""}
204         } else {
205                 m.history[historyType] = append(m.history[historyType], "")
206         }
207         m.historyNum = len(m.history[historyType]) - 1
208
209         response, canceled := "", true
210
211         RedrawAll()
212         for m.hasPrompt {
213                 var suggestions []string
214                 m.Clear()
215
216                 event := <-events
217
218                 switch e := event.(type) {
219                 case *tcell.EventKey:
220                         switch e.Key() {
221                         case tcell.KeyCtrlQ, tcell.KeyCtrlC, tcell.KeyEscape:
222                                 // Cancel
223                                 m.AddLog("\t--> (cancel)")
224                                 m.hasPrompt = false
225                         case tcell.KeyEnter:
226                                 // User is done entering their response
227                                 m.AddLog("\t--> " + m.response)
228                                 m.hasPrompt = false
229                                 response, canceled = m.response, false
230                                 m.history[historyType][len(m.history[historyType])-1] = response
231                         case tcell.KeyTab:
232                                 args := SplitCommandArgs(m.response)
233                                 currentArgNum := len(args) - 1
234                                 currentArg := args[currentArgNum]
235                                 var completionType Completion
236
237                                 if completionTypes[0] == CommandCompletion && currentArgNum > 0 {
238                                         if command, ok := commands[args[0]]; ok {
239                                                 completionTypes = append([]Completion{CommandCompletion}, command.completions...)
240                                         }
241                                 }
242
243                                 if currentArgNum >= len(completionTypes) {
244                                         completionType = completionTypes[len(completionTypes)-1]
245                                 } else {
246                                         completionType = completionTypes[currentArgNum]
247                                 }
248
249                                 var chosen string
250                                 if completionType == FileCompletion {
251                                         chosen, suggestions = FileComplete(currentArg)
252                                 } else if completionType == CommandCompletion {
253                                         chosen, suggestions = CommandComplete(currentArg)
254                                 } else if completionType == HelpCompletion {
255                                         chosen, suggestions = HelpComplete(currentArg)
256                                 } else if completionType == OptionCompletion {
257                                         chosen, suggestions = OptionComplete(currentArg)
258                                 } else if completionType < NoCompletion {
259                                         chosen, suggestions = PluginComplete(completionType, currentArg)
260                                 }
261
262                                 if len(suggestions) > 1 {
263                                         chosen = chosen + CommonSubstring(suggestions...)
264                                 }
265
266                                 if chosen != "" {
267                                         m.response = JoinCommandArgs(append(args[:len(args)-1], chosen)...)
268                                         m.cursorx = Count(m.response)
269                                 }
270                         }
271                 }
272
273                 m.HandleEvent(event, m.history[historyType])
274
275                 m.Clear()
276                 for _, v := range tabs[curTab].views {
277                         v.Display()
278                 }
279                 DisplayTabs()
280                 m.Display()
281                 if len(suggestions) > 1 {
282                         m.DisplaySuggestions(suggestions)
283                 }
284                 screen.Show()
285         }
286
287         m.Clear()
288         m.Reset()
289         return response, canceled
290 }
291
292 // HandleEvent handles an event for the prompter
293 func (m *Messenger) HandleEvent(event tcell.Event, history []string) {
294         switch e := event.(type) {
295         case *tcell.EventKey:
296                 if e.Key() != tcell.KeyRune || e.Modifiers() != 0 {
297                         for key, actions := range bindings {
298                                 if e.Key() == key.keyCode {
299                                         if e.Key() == tcell.KeyRune {
300                                                 if e.Rune() != key.r {
301                                                         continue
302                                                 }
303                                         }
304                                         if e.Modifiers() == key.modifiers {
305                                                 for _, action := range actions {
306                                                         funcName := FuncName(action)
307                                                         switch funcName {
308                                                         case "main.(*View).CursorUp":
309                                                                 if m.historyNum > 0 {
310                                                                         m.historyNum--
311                                                                         m.response = history[m.historyNum]
312                                                                         m.cursorx = Count(m.response)
313                                                                 }
314                                                         case "main.(*View).CursorDown":
315                                                                 if m.historyNum < len(history)-1 {
316                                                                         m.historyNum++
317                                                                         m.response = history[m.historyNum]
318                                                                         m.cursorx = Count(m.response)
319                                                                 }
320                                                         case "main.(*View).CursorLeft":
321                                                                 if m.cursorx > 0 {
322                                                                         m.cursorx--
323                                                                 }
324                                                         case "main.(*View).CursorRight":
325                                                                 if m.cursorx < Count(m.response) {
326                                                                         m.cursorx++
327                                                                 }
328                                                         case "main.(*View).CursorStart", "main.(*View).StartOfLine":
329                                                                 m.cursorx = 0
330                                                         case "main.(*View).CursorEnd", "main.(*View).EndOfLine":
331                                                                 m.cursorx = Count(m.response)
332                                                         case "main.(*View).Backspace":
333                                                                 if m.cursorx > 0 {
334                                                                         m.response = string([]rune(m.response)[:m.cursorx-1]) + string([]rune(m.response)[m.cursorx:])
335                                                                         m.cursorx--
336                                                                 }
337                                                         case "main.(*View).Paste":
338                                                                 clip, _ := clipboard.ReadAll("clipboard")
339                                                                 m.response = Insert(m.response, m.cursorx, clip)
340                                                                 m.cursorx += Count(clip)
341                                                         }
342                                                 }
343                                         }
344                                 }
345                         }
346                 }
347                 switch e.Key() {
348                 case tcell.KeyRune:
349                         m.response = Insert(m.response, m.cursorx, string(e.Rune()))
350                         m.cursorx++
351                 }
352                 history[m.historyNum] = m.response
353
354         case *tcell.EventPaste:
355                 clip := e.Text()
356                 m.response = Insert(m.response, m.cursorx, clip)
357                 m.cursorx += Count(clip)
358         case *tcell.EventMouse:
359                 x, y := e.Position()
360                 x -= Count(m.message)
361                 button := e.Buttons()
362                 _, screenH := screen.Size()
363
364                 if y == screenH-1 {
365                         switch button {
366                         case tcell.Button1:
367                                 m.cursorx = x
368                                 if m.cursorx < 0 {
369                                         m.cursorx = 0
370                                 } else if m.cursorx > Count(m.response) {
371                                         m.cursorx = Count(m.response)
372                                 }
373                         }
374                 }
375         }
376 }
377
378 // Reset resets the messenger's cursor, message and response
379 func (m *Messenger) Reset() {
380         m.cursorx = 0
381         m.message = ""
382         m.response = ""
383 }
384
385 // Clear clears the line at the bottom of the editor
386 func (m *Messenger) Clear() {
387         w, h := screen.Size()
388         for x := 0; x < w; x++ {
389                 screen.SetContent(x, h-1, ' ', nil, defStyle)
390         }
391 }
392
393 func (m *Messenger) DisplaySuggestions(suggestions []string) {
394         w, screenH := screen.Size()
395
396         y := screenH - 2
397
398         statusLineStyle := defStyle.Reverse(true)
399         if style, ok := colorscheme["statusline"]; ok {
400                 statusLineStyle = style
401         }
402
403         for x := 0; x < w; x++ {
404                 screen.SetContent(x, y, ' ', nil, statusLineStyle)
405         }
406
407         x := 0
408         for _, suggestion := range suggestions {
409                 for _, c := range suggestion {
410                         screen.SetContent(x, y, c, nil, statusLineStyle)
411                         x++
412                 }
413                 screen.SetContent(x, y, ' ', nil, statusLineStyle)
414                 x++
415         }
416 }
417
418 // Display displays messages or prompts
419 func (m *Messenger) Display() {
420         _, h := screen.Size()
421         if m.hasMessage {
422                 if m.hasPrompt || globalSettings["infobar"].(bool) {
423                         runes := []rune(m.message + m.response)
424                         for x := 0; x < len(runes); x++ {
425                                 screen.SetContent(x, h-1, runes[x], nil, m.style)
426                         }
427                 }
428         }
429
430         if m.hasPrompt {
431                 screen.ShowCursor(Count(m.message)+m.cursorx, h-1)
432                 screen.Show()
433         }
434 }
435
436 // A GutterMessage is a message displayed on the side of the editor
437 type GutterMessage struct {
438         lineNum int
439         msg     string
440         kind    int
441 }
442
443 // These are the different types of messages
444 const (
445         // GutterInfo represents a simple info message
446         GutterInfo = iota
447         // GutterWarning represents a compiler warning
448         GutterWarning
449         // GutterError represents a compiler error
450         GutterError
451 )