]> git.lizzy.rs Git - micro.git/blob - cmd/micro/messenger.go
d6ad3b1067c70f9825e20a39bd6293cfcd067970
[micro.git] / cmd / micro / messenger.go
1 package main
2
3 import (
4         "bufio"
5         "bytes"
6         "encoding/gob"
7         "fmt"
8         "os"
9         "strconv"
10
11         "github.com/mattn/go-runewidth"
12         "github.com/zyedidia/clipboard"
13         "github.com/zyedidia/micro/cmd/micro/shellwords"
14         "github.com/zyedidia/tcell"
15 )
16
17 // TermMessage sends a message to the user in the terminal. This usually occurs before
18 // micro has been fully initialized -- ie if there is an error in the syntax highlighting
19 // regular expressions
20 // The function must be called when the screen is not initialized
21 // This will write the message, and wait for the user
22 // to press and key to continue
23 func TermMessage(msg ...interface{}) {
24         screenWasNil := screen == nil
25         if !screenWasNil {
26                 screen.Fini()
27                 screen = nil
28         }
29
30         fmt.Println(msg...)
31         fmt.Print("\nPress enter to continue")
32
33         reader := bufio.NewReader(os.Stdin)
34         reader.ReadString('\n')
35
36         if !screenWasNil {
37                 InitScreen()
38         }
39 }
40
41 // TermError sends an error to the user in the terminal. Like TermMessage except formatted
42 // as an error
43 func TermError(filename string, lineNum int, err string) {
44         TermMessage(filename + ", " + strconv.Itoa(lineNum) + ": " + err)
45 }
46
47 // Messenger is an object that makes it easy to send messages to the user
48 // and get input from the user
49 type Messenger struct {
50         log *Buffer
51         // Are we currently prompting the user?
52         hasPrompt bool
53         // Is there a message to print
54         hasMessage bool
55
56         // Message to print
57         message string
58         // The user's response to a prompt
59         response string
60         // style to use when drawing the message
61         style tcell.Style
62
63         // We have to keep track of the cursor for prompting
64         cursorx int
65
66         // This map stores the history for all the different kinds of uses Prompt has
67         // It's a map of history type -> history array
68         history    map[string][]string
69         historyNum int
70
71         // Is the current message a message from the gutter
72         gutterMessage bool
73 }
74
75 // AddLog sends a message to the log view
76 func (m *Messenger) AddLog(msg ...interface{}) {
77         logMessage := fmt.Sprint(msg...)
78         buffer := m.getBuffer()
79         buffer.insert(buffer.End(), []byte(logMessage+"\n"))
80         buffer.Cursor.Loc = buffer.End()
81         buffer.Cursor.Relocate()
82 }
83
84 func (m *Messenger) getBuffer() *Buffer {
85         if m.log == nil {
86                 m.log = NewBufferFromString("", "")
87                 m.log.name = "Log"
88         }
89         return m.log
90 }
91
92 // Message sends a message to the user
93 func (m *Messenger) Message(msg ...interface{}) {
94         displayMessage := fmt.Sprint(msg...)
95         // only display a new message if there isn't an active prompt
96         // this is to prevent overwriting an existing prompt to the user
97         if m.hasPrompt == false {
98                 // if there is no active prompt then style and display the message as normal
99                 m.message = displayMessage
100
101                 m.style = defStyle
102
103                 if _, ok := colorscheme["message"]; ok {
104                         m.style = colorscheme["message"]
105                 }
106
107                 m.hasMessage = true
108         }
109         // add the message to the log regardless of active prompts
110         m.AddLog(displayMessage)
111 }
112
113 // Error sends an error message to the user
114 func (m *Messenger) Error(msg ...interface{}) {
115         buf := new(bytes.Buffer)
116         fmt.Fprint(buf, msg...)
117
118         // only display a new message if there isn't an active prompt
119         // this is to prevent overwriting an existing prompt to the user
120         if m.hasPrompt == false {
121                 // if there is no active prompt then style and display the message as normal
122                 m.message = buf.String()
123                 m.style = defStyle.
124                         Foreground(tcell.ColorBlack).
125                         Background(tcell.ColorMaroon)
126
127                 if _, ok := colorscheme["error-message"]; ok {
128                         m.style = colorscheme["error-message"]
129                 }
130                 m.hasMessage = true
131         }
132         // add the message to the log regardless of active prompts
133         m.AddLog(buf.String())
134 }
135
136 func (m *Messenger) PromptText(msg ...interface{}) {
137         displayMessage := fmt.Sprint(msg...)
138         // if there is no active prompt then style and display the message as normal
139         m.message = displayMessage
140
141         m.style = defStyle
142
143         if _, ok := colorscheme["message"]; ok {
144                 m.style = colorscheme["message"]
145         }
146
147         m.hasMessage = true
148         // add the message to the log regardless of active prompts
149         m.AddLog(displayMessage)
150 }
151
152 // YesNoPrompt asks the user a yes or no question (waits for y or n) and returns the result
153 func (m *Messenger) YesNoPrompt(prompt string) (bool, bool) {
154         m.hasPrompt = true
155         m.PromptText(prompt)
156
157         _, h := screen.Size()
158         for {
159                 m.Clear()
160                 m.Display()
161                 screen.ShowCursor(Count(m.message), h-1)
162                 screen.Show()
163                 event := <-events
164
165                 switch e := event.(type) {
166                 case *tcell.EventKey:
167                         switch e.Key() {
168                         case tcell.KeyRune:
169                                 if e.Rune() == 'y' || e.Rune() == 'Y' {
170                                         m.AddLog("\t--> y")
171                                         m.hasPrompt = false
172                                         return true, false
173                                 } else if e.Rune() == 'n' || e.Rune() == 'N' {
174                                         m.AddLog("\t--> n")
175                                         m.hasPrompt = false
176                                         return false, false
177                                 }
178                         case tcell.KeyCtrlC, tcell.KeyCtrlQ, tcell.KeyEscape:
179                                 m.AddLog("\t--> (cancel)")
180                                 m.Clear()
181                                 m.Reset()
182                                 m.hasPrompt = false
183                                 return false, true
184                         }
185                 }
186         }
187 }
188
189 // LetterPrompt gives the user a prompt and waits for a one letter response
190 func (m *Messenger) LetterPrompt(prompt string, responses ...rune) (rune, bool) {
191         m.hasPrompt = true
192         m.PromptText(prompt)
193
194         _, h := screen.Size()
195         for {
196                 m.Clear()
197                 m.Display()
198                 screen.ShowCursor(Count(m.message), h-1)
199                 screen.Show()
200                 event := <-events
201
202                 switch e := event.(type) {
203                 case *tcell.EventKey:
204                         switch e.Key() {
205                         case tcell.KeyRune:
206                                 for _, r := range responses {
207                                         if e.Rune() == r {
208                                                 m.AddLog("\t--> " + string(r))
209                                                 m.Clear()
210                                                 m.Reset()
211                                                 m.hasPrompt = false
212                                                 return r, false
213                                         }
214                                 }
215                         case tcell.KeyCtrlC, tcell.KeyCtrlQ, tcell.KeyEscape:
216                                 m.AddLog("\t--> (cancel)")
217                                 m.Clear()
218                                 m.Reset()
219                                 m.hasPrompt = false
220                                 return ' ', true
221                         }
222                 }
223         }
224 }
225
226 // Completion represents a type of completion
227 type Completion int
228
229 const (
230         NoCompletion Completion = iota
231         FileCompletion
232         CommandCompletion
233         HelpCompletion
234         OptionCompletion
235         PluginCmdCompletion
236         PluginNameCompletion
237         OptionValueCompletion
238 )
239
240 // Prompt sends the user a message and waits for a response to be typed in
241 // This function blocks the main loop while waiting for input
242 func (m *Messenger) Prompt(prompt, placeholder, historyType string, completionTypes ...Completion) (string, bool) {
243         m.hasPrompt = true
244         m.PromptText(prompt)
245         if _, ok := m.history[historyType]; !ok {
246                 m.history[historyType] = []string{""}
247         } else {
248                 m.history[historyType] = append(m.history[historyType], "")
249         }
250         m.historyNum = len(m.history[historyType]) - 1
251
252         response, canceled := placeholder, true
253         m.response = response
254         m.cursorx = Count(placeholder)
255
256         RedrawAll()
257         for m.hasPrompt {
258                 var suggestions []string
259                 m.Clear()
260
261                 event := <-events
262
263                 switch e := event.(type) {
264                 case *tcell.EventKey:
265                         switch e.Key() {
266                         case tcell.KeyCtrlQ, tcell.KeyCtrlC, tcell.KeyEscape:
267                                 // Cancel
268                                 m.AddLog("\t--> (cancel)")
269                                 m.hasPrompt = false
270                         case tcell.KeyEnter:
271                                 // User is done entering their response
272                                 m.AddLog("\t--> " + m.response)
273                                 m.hasPrompt = false
274                                 response, canceled = m.response, false
275                                 m.history[historyType][len(m.history[historyType])-1] = response
276                         case tcell.KeyTab:
277                                 args, err := shellwords.Split(m.response)
278                                 if err != nil {
279                                         break
280                                 }
281                                 currentArg := ""
282                                 currentArgNum := 0
283                                 if len(args) > 0 {
284                                         currentArgNum = len(args) - 1
285                                         currentArg = args[currentArgNum]
286                                 }
287                                 var completionType Completion
288
289                                 if completionTypes[0] == CommandCompletion && currentArgNum > 0 {
290                                         if command, ok := commands[args[0]]; ok {
291                                                 completionTypes = append([]Completion{CommandCompletion}, command.completions...)
292                                         }
293                                 }
294
295                                 if currentArgNum >= len(completionTypes) {
296                                         completionType = completionTypes[len(completionTypes)-1]
297                                 } else {
298                                         completionType = completionTypes[currentArgNum]
299                                 }
300
301                                 var chosen string
302                                 if completionType == FileCompletion {
303                                         chosen, suggestions = FileComplete(currentArg)
304                                 } else if completionType == CommandCompletion {
305                                         chosen, suggestions = CommandComplete(currentArg)
306                                 } else if completionType == HelpCompletion {
307                                         chosen, suggestions = HelpComplete(currentArg)
308                                 } else if completionType == OptionCompletion {
309                                         chosen, suggestions = OptionComplete(currentArg)
310                                 } else if completionType == OptionValueCompletion {
311                                         if currentArgNum-1 > 0 {
312                                                 chosen, suggestions = OptionValueComplete(args[currentArgNum-1], currentArg)
313                                         }
314                                 } else if completionType == PluginCmdCompletion {
315                                         chosen, suggestions = PluginCmdComplete(currentArg)
316                                 } else if completionType == PluginNameCompletion {
317                                         chosen, suggestions = PluginNameComplete(currentArg)
318                                 } else if completionType < NoCompletion {
319                                         chosen, suggestions = PluginComplete(completionType, currentArg)
320                                 }
321
322                                 if len(suggestions) > 1 {
323                                         chosen = chosen + CommonSubstring(suggestions...)
324                                 }
325
326                                 if len(suggestions) != 0 && chosen != "" {
327                                         m.response = shellwords.Join(append(args[:len(args)-1], chosen)...)
328                                         m.cursorx = Count(m.response)
329                                 }
330                         }
331                 }
332
333                 m.HandleEvent(event, m.history[historyType])
334
335                 m.Clear()
336                 for _, v := range tabs[curTab].views {
337                         v.Display()
338                 }
339                 DisplayTabs()
340                 m.Display()
341                 if len(suggestions) > 1 {
342                         m.DisplaySuggestions(suggestions)
343                 }
344                 screen.Show()
345         }
346
347         m.Clear()
348         m.Reset()
349         return response, canceled
350 }
351
352 // UpHistory fetches the previous item in the history
353 func (m *Messenger) UpHistory(history []string) {
354         if m.historyNum > 0 {
355                 m.historyNum--
356                 m.response = history[m.historyNum]
357                 m.cursorx = Count(m.response)
358         }
359 }
360
361 // DownHistory fetches the next item in the history
362 func (m *Messenger) DownHistory(history []string) {
363         if m.historyNum < len(history)-1 {
364                 m.historyNum++
365                 m.response = history[m.historyNum]
366                 m.cursorx = Count(m.response)
367         }
368 }
369
370 // CursorLeft moves the cursor one character left
371 func (m *Messenger) CursorLeft() {
372         if m.cursorx > 0 {
373                 m.cursorx--
374         }
375 }
376
377 // CursorRight moves the cursor one character right
378 func (m *Messenger) CursorRight() {
379         if m.cursorx < Count(m.response) {
380                 m.cursorx++
381         }
382 }
383
384 // Start moves the cursor to the start of the line
385 func (m *Messenger) Start() {
386         m.cursorx = 0
387 }
388
389 // End moves the cursor to the end of the line
390 func (m *Messenger) End() {
391         m.cursorx = Count(m.response)
392 }
393
394 // Backspace deletes one character
395 func (m *Messenger) Backspace() {
396         if m.cursorx > 0 {
397                 m.response = string([]rune(m.response)[:m.cursorx-1]) + string([]rune(m.response)[m.cursorx:])
398                 m.cursorx--
399         }
400 }
401
402 // Paste pastes the clipboard
403 func (m *Messenger) Paste() {
404         clip, _ := clipboard.ReadAll("clipboard")
405         m.response = Insert(m.response, m.cursorx, clip)
406         m.cursorx += Count(clip)
407 }
408
409 // WordLeft moves the cursor one word to the left
410 func (m *Messenger) WordLeft() {
411         response := []rune(m.response)
412         m.CursorLeft()
413         if m.cursorx <= 0 {
414                 return
415         }
416         for IsWhitespace(response[m.cursorx]) {
417                 if m.cursorx <= 0 {
418                         return
419                 }
420                 m.CursorLeft()
421         }
422         m.CursorLeft()
423         for IsWordChar(string(response[m.cursorx])) {
424                 if m.cursorx <= 0 {
425                         return
426                 }
427                 m.CursorLeft()
428         }
429         m.CursorRight()
430 }
431
432 // WordRight moves the cursor one word to the right
433 func (m *Messenger) WordRight() {
434         response := []rune(m.response)
435         if m.cursorx >= len(response) {
436                 return
437         }
438         for IsWhitespace(response[m.cursorx]) {
439                 m.CursorRight()
440                 if m.cursorx >= len(response) {
441                         m.CursorRight()
442                         return
443                 }
444         }
445         m.CursorRight()
446         if m.cursorx >= len(response) {
447                 return
448         }
449         for IsWordChar(string(response[m.cursorx])) {
450                 m.CursorRight()
451                 if m.cursorx >= len(response) {
452                         return
453                 }
454         }
455 }
456
457 // DeleteWordLeft deletes one word to the left
458 func (m *Messenger) DeleteWordLeft() {
459         m.WordLeft()
460         m.response = string([]rune(m.response)[:m.cursorx])
461 }
462
463 // HandleEvent handles an event for the prompter
464 func (m *Messenger) HandleEvent(event tcell.Event, history []string) {
465         switch e := event.(type) {
466         case *tcell.EventKey:
467                 switch e.Key() {
468                 case tcell.KeyCtrlA:
469                         m.Start()
470                 case tcell.KeyCtrlE:
471                         m.End()
472                 case tcell.KeyUp:
473                         m.UpHistory(history)
474                 case tcell.KeyDown:
475                         m.DownHistory(history)
476                 case tcell.KeyLeft:
477                         if e.Modifiers() == tcell.ModCtrl {
478                                 m.Start()
479                         } else if e.Modifiers() == tcell.ModAlt || e.Modifiers() == tcell.ModMeta {
480                                 m.WordLeft()
481                         } else {
482                                 m.CursorLeft()
483                         }
484                 case tcell.KeyRight:
485                         if e.Modifiers() == tcell.ModCtrl {
486                                 m.End()
487                         } else if e.Modifiers() == tcell.ModAlt || e.Modifiers() == tcell.ModMeta {
488                                 m.WordRight()
489                         } else {
490                                 m.CursorRight()
491                         }
492                 case tcell.KeyBackspace2, tcell.KeyBackspace:
493                         if e.Modifiers() == tcell.ModCtrl || e.Modifiers() == tcell.ModAlt || e.Modifiers() == tcell.ModMeta {
494                                 m.DeleteWordLeft()
495                         } else {
496                                 m.Backspace()
497                         }
498                 case tcell.KeyCtrlW:
499                         m.DeleteWordLeft()
500                 case tcell.KeyCtrlV:
501                         m.Paste()
502                 case tcell.KeyCtrlF:
503                         m.WordRight()
504                 case tcell.KeyCtrlB:
505                         m.WordLeft()
506                 case tcell.KeyRune:
507                         m.response = Insert(m.response, m.cursorx, string(e.Rune()))
508                         m.cursorx++
509                 }
510                 history[m.historyNum] = m.response
511
512         case *tcell.EventPaste:
513                 clip := e.Text()
514                 m.response = Insert(m.response, m.cursorx, clip)
515                 m.cursorx += Count(clip)
516         case *tcell.EventMouse:
517                 x, y := e.Position()
518                 x -= Count(m.message)
519                 button := e.Buttons()
520                 _, screenH := screen.Size()
521
522                 if y == screenH-1 {
523                         switch button {
524                         case tcell.Button1:
525                                 m.cursorx = x
526                                 if m.cursorx < 0 {
527                                         m.cursorx = 0
528                                 } else if m.cursorx > Count(m.response) {
529                                         m.cursorx = Count(m.response)
530                                 }
531                         }
532                 }
533         }
534 }
535
536 // Reset resets the messenger's cursor, message and response
537 func (m *Messenger) Reset() {
538         m.cursorx = 0
539         m.message = ""
540         m.response = ""
541 }
542
543 // Clear clears the line at the bottom of the editor
544 func (m *Messenger) Clear() {
545         w, h := screen.Size()
546         for x := 0; x < w; x++ {
547                 screen.SetContent(x, h-1, ' ', nil, defStyle)
548         }
549 }
550
551 func (m *Messenger) DisplaySuggestions(suggestions []string) {
552         w, screenH := screen.Size()
553
554         y := screenH - 2
555
556         statusLineStyle := defStyle.Reverse(true)
557         if style, ok := colorscheme["statusline"]; ok {
558                 statusLineStyle = style
559         }
560
561         for x := 0; x < w; x++ {
562                 screen.SetContent(x, y, ' ', nil, statusLineStyle)
563         }
564
565         x := 0
566         for _, suggestion := range suggestions {
567                 for _, c := range suggestion {
568                         screen.SetContent(x, y, c, nil, statusLineStyle)
569                         x++
570                 }
571                 screen.SetContent(x, y, ' ', nil, statusLineStyle)
572                 x++
573         }
574 }
575
576 // Display displays messages or prompts
577 func (m *Messenger) Display() {
578         _, h := screen.Size()
579         if m.hasMessage {
580                 if m.hasPrompt || globalSettings["infobar"].(bool) {
581                         runes := []rune(m.message + m.response)
582                         posx := 0
583                         for x := 0; x < len(runes); x++ {
584                                 screen.SetContent(posx, h-1, runes[x], nil, m.style)
585                                 posx += runewidth.RuneWidth(runes[x])
586                         }
587                 }
588         }
589
590         if m.hasPrompt {
591                 screen.ShowCursor(Count(m.message)+m.cursorx, h-1)
592                 screen.Show()
593         }
594 }
595
596 // LoadHistory attempts to load user history from configDir/buffers/history
597 // into the history map
598 // The savehistory option must be on
599 func (m *Messenger) LoadHistory() {
600         if GetGlobalOption("savehistory").(bool) {
601                 file, err := os.Open(configDir + "/buffers/history")
602                 var decodedMap map[string][]string
603                 if err == nil {
604                         decoder := gob.NewDecoder(file)
605                         err = decoder.Decode(&decodedMap)
606                         file.Close()
607
608                         if err != nil {
609                                 m.Error("Error loading history:", err)
610                                 return
611                         }
612                 }
613
614                 if decodedMap != nil {
615                         m.history = decodedMap
616                 } else {
617                         m.history = make(map[string][]string)
618                 }
619         } else {
620                 m.history = make(map[string][]string)
621         }
622 }
623
624 // SaveHistory saves the user's command history to configDir/buffers/history
625 // only if the savehistory option is on
626 func (m *Messenger) SaveHistory() {
627         if GetGlobalOption("savehistory").(bool) {
628                 // Don't save history past 100
629                 for k, v := range m.history {
630                         if len(v) > 100 {
631                                 m.history[k] = v[len(m.history[k])-100:]
632                         }
633                 }
634
635                 file, err := os.Create(configDir + "/buffers/history")
636                 if err == nil {
637                         encoder := gob.NewEncoder(file)
638
639                         err = encoder.Encode(m.history)
640                         if err != nil {
641                                 m.Error("Error saving history:", err)
642                                 return
643                         }
644                         file.Close()
645                 }
646         }
647 }
648
649 // A GutterMessage is a message displayed on the side of the editor
650 type GutterMessage struct {
651         lineNum int
652         msg     string
653         kind    int
654 }
655
656 // These are the different types of messages
657 const (
658         // GutterInfo represents a simple info message
659         GutterInfo = iota
660         // GutterWarning represents a compiler warning
661         GutterWarning
662         // GutterError represents a compiler error
663         GutterError
664 )