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