]> git.lizzy.rs Git - micro.git/blob - cmd/micro/messenger.go
Update readme
[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.EventResize:
265                         for _, t := range tabs {
266                                 t.Resize()
267                         }
268                         RedrawAll()
269                 case *tcell.EventKey:
270                         switch e.Key() {
271                         case tcell.KeyCtrlQ, tcell.KeyCtrlC, tcell.KeyEscape:
272                                 // Cancel
273                                 m.AddLog("\t--> (cancel)")
274                                 m.hasPrompt = false
275                         case tcell.KeyEnter:
276                                 // User is done entering their response
277                                 m.AddLog("\t--> " + m.response)
278                                 m.hasPrompt = false
279                                 response, canceled = m.response, false
280                                 m.history[historyType][len(m.history[historyType])-1] = response
281                         case tcell.KeyTab:
282                                 args, err := shellwords.Split(m.response)
283                                 if err != nil {
284                                         break
285                                 }
286                                 currentArg := ""
287                                 currentArgNum := 0
288                                 if len(args) > 0 {
289                                         currentArgNum = len(args) - 1
290                                         currentArg = args[currentArgNum]
291                                 }
292                                 var completionType Completion
293
294                                 if completionTypes[0] == CommandCompletion && currentArgNum > 0 {
295                                         if command, ok := commands[args[0]]; ok {
296                                                 completionTypes = append([]Completion{CommandCompletion}, command.completions...)
297                                         }
298                                 }
299
300                                 if currentArgNum >= len(completionTypes) {
301                                         completionType = completionTypes[len(completionTypes)-1]
302                                 } else {
303                                         completionType = completionTypes[currentArgNum]
304                                 }
305
306                                 var chosen string
307                                 if completionType == FileCompletion {
308                                         chosen, suggestions = FileComplete(currentArg)
309                                 } else if completionType == CommandCompletion {
310                                         chosen, suggestions = CommandComplete(currentArg)
311                                 } else if completionType == HelpCompletion {
312                                         chosen, suggestions = HelpComplete(currentArg)
313                                 } else if completionType == OptionCompletion {
314                                         chosen, suggestions = OptionComplete(currentArg)
315                                 } else if completionType == OptionValueCompletion {
316                                         if currentArgNum-1 > 0 {
317                                                 chosen, suggestions = OptionValueComplete(args[currentArgNum-1], currentArg)
318                                         }
319                                 } else if completionType == PluginCmdCompletion {
320                                         chosen, suggestions = PluginCmdComplete(currentArg)
321                                 } else if completionType == PluginNameCompletion {
322                                         chosen, suggestions = PluginNameComplete(currentArg)
323                                 } else if completionType < NoCompletion {
324                                         chosen, suggestions = PluginComplete(completionType, currentArg)
325                                 }
326
327                                 if len(suggestions) > 1 {
328                                         chosen = chosen + CommonSubstring(suggestions...)
329                                 }
330
331                                 if len(suggestions) != 0 && chosen != "" {
332                                         m.response = shellwords.Join(append(args[:len(args)-1], chosen)...)
333                                         m.cursorx = Count(m.response)
334                                 }
335                         }
336                 }
337
338                 m.HandleEvent(event, m.history[historyType])
339
340                 m.Clear()
341                 for _, v := range tabs[curTab].Views {
342                         v.Display()
343                 }
344                 DisplayTabs()
345                 m.Display()
346                 if len(suggestions) > 1 {
347                         m.DisplaySuggestions(suggestions)
348                 }
349                 screen.Show()
350         }
351
352         m.Clear()
353         m.Reset()
354         return response, canceled
355 }
356
357 // UpHistory fetches the previous item in the history
358 func (m *Messenger) UpHistory(history []string) {
359         if m.historyNum > 0 {
360                 m.historyNum--
361                 m.response = history[m.historyNum]
362                 m.cursorx = Count(m.response)
363         }
364 }
365
366 // DownHistory fetches the next item in the history
367 func (m *Messenger) DownHistory(history []string) {
368         if m.historyNum < len(history)-1 {
369                 m.historyNum++
370                 m.response = history[m.historyNum]
371                 m.cursorx = Count(m.response)
372         }
373 }
374
375 // CursorLeft moves the cursor one character left
376 func (m *Messenger) CursorLeft() {
377         if m.cursorx > 0 {
378                 m.cursorx--
379         }
380 }
381
382 // CursorRight moves the cursor one character right
383 func (m *Messenger) CursorRight() {
384         if m.cursorx < Count(m.response) {
385                 m.cursorx++
386         }
387 }
388
389 // Start moves the cursor to the start of the line
390 func (m *Messenger) Start() {
391         m.cursorx = 0
392 }
393
394 // End moves the cursor to the end of the line
395 func (m *Messenger) End() {
396         m.cursorx = Count(m.response)
397 }
398
399 // Backspace deletes one character
400 func (m *Messenger) Backspace() {
401         if m.cursorx > 0 {
402                 m.response = string([]rune(m.response)[:m.cursorx-1]) + string([]rune(m.response)[m.cursorx:])
403                 m.cursorx--
404         }
405 }
406
407 // Paste pastes the clipboard
408 func (m *Messenger) Paste() {
409         clip, _ := clipboard.ReadAll("clipboard")
410         m.response = Insert(m.response, m.cursorx, clip)
411         m.cursorx += Count(clip)
412 }
413
414 // WordLeft moves the cursor one word to the left
415 func (m *Messenger) WordLeft() {
416         response := []rune(m.response)
417         m.CursorLeft()
418         if m.cursorx <= 0 {
419                 return
420         }
421         for IsWhitespace(response[m.cursorx]) {
422                 if m.cursorx <= 0 {
423                         return
424                 }
425                 m.CursorLeft()
426         }
427         m.CursorLeft()
428         for IsWordChar(string(response[m.cursorx])) {
429                 if m.cursorx <= 0 {
430                         return
431                 }
432                 m.CursorLeft()
433         }
434         m.CursorRight()
435 }
436
437 // WordRight moves the cursor one word to the right
438 func (m *Messenger) WordRight() {
439         response := []rune(m.response)
440         if m.cursorx >= len(response) {
441                 return
442         }
443         for IsWhitespace(response[m.cursorx]) {
444                 m.CursorRight()
445                 if m.cursorx >= len(response) {
446                         m.CursorRight()
447                         return
448                 }
449         }
450         m.CursorRight()
451         if m.cursorx >= len(response) {
452                 return
453         }
454         for IsWordChar(string(response[m.cursorx])) {
455                 m.CursorRight()
456                 if m.cursorx >= len(response) {
457                         return
458                 }
459         }
460 }
461
462 // DeleteWordLeft deletes one word to the left
463 func (m *Messenger) DeleteWordLeft() {
464         m.WordLeft()
465         m.response = string([]rune(m.response)[:m.cursorx])
466 }
467
468 // HandleEvent handles an event for the prompter
469 func (m *Messenger) HandleEvent(event tcell.Event, history []string) {
470         switch e := event.(type) {
471         case *tcell.EventKey:
472                 switch e.Key() {
473                 case tcell.KeyCtrlA:
474                         m.Start()
475                 case tcell.KeyCtrlE:
476                         m.End()
477                 case tcell.KeyUp:
478                         m.UpHistory(history)
479                 case tcell.KeyDown:
480                         m.DownHistory(history)
481                 case tcell.KeyLeft:
482                         if e.Modifiers() == tcell.ModCtrl {
483                                 m.Start()
484                         } else if e.Modifiers() == tcell.ModAlt || e.Modifiers() == tcell.ModMeta {
485                                 m.WordLeft()
486                         } else {
487                                 m.CursorLeft()
488                         }
489                 case tcell.KeyRight:
490                         if e.Modifiers() == tcell.ModCtrl {
491                                 m.End()
492                         } else if e.Modifiers() == tcell.ModAlt || e.Modifiers() == tcell.ModMeta {
493                                 m.WordRight()
494                         } else {
495                                 m.CursorRight()
496                         }
497                 case tcell.KeyBackspace2, tcell.KeyBackspace:
498                         if e.Modifiers() == tcell.ModCtrl || e.Modifiers() == tcell.ModAlt || e.Modifiers() == tcell.ModMeta {
499                                 m.DeleteWordLeft()
500                         } else {
501                                 m.Backspace()
502                         }
503                 case tcell.KeyCtrlW:
504                         m.DeleteWordLeft()
505                 case tcell.KeyCtrlV:
506                         m.Paste()
507                 case tcell.KeyCtrlF:
508                         m.WordRight()
509                 case tcell.KeyCtrlB:
510                         m.WordLeft()
511                 case tcell.KeyRune:
512                         m.response = Insert(m.response, m.cursorx, string(e.Rune()))
513                         m.cursorx++
514                 }
515                 history[m.historyNum] = m.response
516
517         case *tcell.EventPaste:
518                 clip := e.Text()
519                 m.response = Insert(m.response, m.cursorx, clip)
520                 m.cursorx += Count(clip)
521         case *tcell.EventMouse:
522                 x, y := e.Position()
523                 x -= Count(m.message)
524                 button := e.Buttons()
525                 _, screenH := screen.Size()
526
527                 if y == screenH-1 {
528                         switch button {
529                         case tcell.Button1:
530                                 m.cursorx = x
531                                 if m.cursorx < 0 {
532                                         m.cursorx = 0
533                                 } else if m.cursorx > Count(m.response) {
534                                         m.cursorx = Count(m.response)
535                                 }
536                         }
537                 }
538         }
539 }
540
541 // Reset resets the messenger's cursor, message and response
542 func (m *Messenger) Reset() {
543         m.cursorx = 0
544         m.message = ""
545         m.response = ""
546 }
547
548 // Clear clears the line at the bottom of the editor
549 func (m *Messenger) Clear() {
550         w, h := screen.Size()
551         for x := 0; x < w; x++ {
552                 screen.SetContent(x, h-1, ' ', nil, defStyle)
553         }
554 }
555
556 func (m *Messenger) DisplaySuggestions(suggestions []string) {
557         w, screenH := screen.Size()
558
559         y := screenH - 2
560
561         statusLineStyle := defStyle.Reverse(true)
562         if style, ok := colorscheme["statusline"]; ok {
563                 statusLineStyle = style
564         }
565
566         for x := 0; x < w; x++ {
567                 screen.SetContent(x, y, ' ', nil, statusLineStyle)
568         }
569
570         x := 0
571         for _, suggestion := range suggestions {
572                 for _, c := range suggestion {
573                         screen.SetContent(x, y, c, nil, statusLineStyle)
574                         x++
575                 }
576                 screen.SetContent(x, y, ' ', nil, statusLineStyle)
577                 x++
578         }
579 }
580
581 // Display displays messages or prompts
582 func (m *Messenger) Display() {
583         _, h := screen.Size()
584         if m.hasMessage {
585                 if m.hasPrompt || globalSettings["infobar"].(bool) {
586                         runes := []rune(m.message + m.response)
587                         posx := 0
588                         for x := 0; x < len(runes); x++ {
589                                 screen.SetContent(posx, h-1, runes[x], nil, m.style)
590                                 posx += runewidth.RuneWidth(runes[x])
591                         }
592                 }
593         }
594
595         if m.hasPrompt {
596                 screen.ShowCursor(Count(m.message)+m.cursorx, h-1)
597                 screen.Show()
598         }
599 }
600
601 // LoadHistory attempts to load user history from configDir/buffers/history
602 // into the history map
603 // The savehistory option must be on
604 func (m *Messenger) LoadHistory() {
605         if GetGlobalOption("savehistory").(bool) {
606                 file, err := os.Open(configDir + "/buffers/history")
607                 defer file.Close()
608                 var decodedMap map[string][]string
609                 if err == nil {
610                         decoder := gob.NewDecoder(file)
611                         err = decoder.Decode(&decodedMap)
612
613                         if err != nil {
614                                 m.Error("Error loading history:", err)
615                                 return
616                         }
617                 }
618
619                 if decodedMap != nil {
620                         m.history = decodedMap
621                 } else {
622                         m.history = make(map[string][]string)
623                 }
624         } else {
625                 m.history = make(map[string][]string)
626         }
627 }
628
629 // SaveHistory saves the user's command history to configDir/buffers/history
630 // only if the savehistory option is on
631 func (m *Messenger) SaveHistory() {
632         if GetGlobalOption("savehistory").(bool) {
633                 // Don't save history past 100
634                 for k, v := range m.history {
635                         if len(v) > 100 {
636                                 m.history[k] = v[len(m.history[k])-100:]
637                         }
638                 }
639
640                 file, err := os.Create(configDir + "/buffers/history")
641                 defer file.Close()
642                 if err == nil {
643                         encoder := gob.NewEncoder(file)
644
645                         err = encoder.Encode(m.history)
646                         if err != nil {
647                                 m.Error("Error saving history:", err)
648                                 return
649                         }
650                 }
651         }
652 }
653
654 // A GutterMessage is a message displayed on the side of the editor
655 type GutterMessage struct {
656         lineNum int
657         msg     string
658         kind    int
659 }
660
661 // These are the different types of messages
662 const (
663         // GutterInfo represents a simple info message
664         GutterInfo = iota
665         // GutterWarning represents a compiler warning
666         GutterWarning
667         // GutterError represents a compiler error
668         GutterError
669 )