]> git.lizzy.rs Git - micro.git/blob - cmd/micro/messenger.go
Fix autocompletion using a slash
[micro.git] / cmd / micro / messenger.go
1 package main
2
3 import (
4         "bufio"
5         "bytes"
6         "fmt"
7         "io/ioutil"
8         "os"
9         "strconv"
10         "strings"
11
12         "github.com/zyedidia/tcell"
13 )
14
15 // TermMessage sends a message to the user in the terminal. This usually occurs before
16 // micro has been fully initialized -- ie if there is an error in the syntax highlighting
17 // regular expressions
18 // The function must be called when the screen is not initialized
19 // This will write the message, and wait for the user
20 // to press and key to continue
21 func TermMessage(msg ...interface{}) {
22         screenWasNil := screen == nil
23         if !screenWasNil {
24                 screen.Fini()
25         }
26
27         fmt.Println(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         // Are we currently prompting the user?
48         hasPrompt bool
49         // Is there a message to print
50         hasMessage bool
51
52         // Message to print
53         message string
54         // The user's response to a prompt
55         response string
56         // style to use when drawing the message
57         style tcell.Style
58
59         // We have to keep track of the cursor for prompting
60         cursorx int
61
62         // This map stores the history for all the different kinds of uses Prompt has
63         // It's a map of history type -> history array
64         history    map[string][]string
65         historyNum int
66
67         // Is the current message a message from the gutter
68         gutterMessage bool
69 }
70
71 // Message sends a message to the user
72 func (m *Messenger) Message(msg ...interface{}) {
73         buf := new(bytes.Buffer)
74         fmt.Fprint(buf, msg...)
75         m.message = buf.String()
76         m.style = defStyle
77
78         if _, ok := colorscheme["message"]; ok {
79                 m.style = colorscheme["message"]
80         }
81         m.hasMessage = true
82 }
83
84 // Error sends an error message to the user
85 func (m *Messenger) Error(msg ...interface{}) {
86         buf := new(bytes.Buffer)
87         fmt.Fprint(buf, msg...)
88         m.message = buf.String()
89         m.style = defStyle.
90                 Foreground(tcell.ColorBlack).
91                 Background(tcell.ColorMaroon)
92
93         if _, ok := colorscheme["error-message"]; ok {
94                 m.style = colorscheme["error-message"]
95         }
96         m.hasMessage = true
97 }
98
99 // YesNoPrompt asks the user a yes or no question (waits for y or n) and returns the result
100 func (m *Messenger) YesNoPrompt(prompt string) (bool, bool) {
101         m.Message(prompt)
102
103         _, h := screen.Size()
104         for {
105                 m.Clear()
106                 m.Display()
107                 screen.ShowCursor(Count(m.message), h-1)
108                 screen.Show()
109                 event := screen.PollEvent()
110
111                 switch e := event.(type) {
112                 case *tcell.EventKey:
113                         switch e.Key() {
114                         case tcell.KeyRune:
115                                 if e.Rune() == 'y' {
116                                         return true, false
117                                 } else if e.Rune() == 'n' {
118                                         return false, false
119                                 }
120                         case tcell.KeyCtrlC, tcell.KeyCtrlQ, tcell.KeyEscape:
121                                 return false, true
122                         }
123                 }
124         }
125 }
126
127 type Completion int
128
129 const (
130         NoCompletion Completion = iota
131         FileCompletion
132 )
133
134 // Prompt sends the user a message and waits for a response to be typed in
135 // This function blocks the main loop while waiting for input
136 func (m *Messenger) Prompt(prompt, historyType string, completionType Completion) (string, bool) {
137         m.hasPrompt = true
138         m.Message(prompt)
139         if _, ok := m.history[historyType]; !ok {
140                 m.history[historyType] = []string{""}
141         } else {
142                 m.history[historyType] = append(m.history[historyType], "")
143         }
144         m.historyNum = len(m.history[historyType]) - 1
145
146         response, canceled := "", true
147
148         for m.hasPrompt {
149                 m.Clear()
150                 m.Display()
151
152                 event := screen.PollEvent()
153
154                 switch e := event.(type) {
155                 case *tcell.EventKey:
156                         switch e.Key() {
157                         case tcell.KeyCtrlQ, tcell.KeyCtrlC, tcell.KeyEscape:
158                                 // Cancel
159                                 m.hasPrompt = false
160                         case tcell.KeyEnter:
161                                 // User is done entering their response
162                                 m.hasPrompt = false
163                                 response, canceled = m.response, false
164                                 m.history[historyType][len(m.history[historyType])-1] = response
165                         case tcell.KeyTab:
166                                 if completionType == FileCompletion {
167                                         dirs := strings.Split(m.response, "/")
168                                         var files []os.FileInfo
169                                         var err error
170                                         if len(dirs) > 1 {
171                                                 files, err = ioutil.ReadDir(strings.Join(dirs[:len(dirs)-1], "/"))
172                                         } else {
173                                                 files, err = ioutil.ReadDir(".")
174                                         }
175                                         if err != nil {
176                                                 continue
177                                         }
178                                         var suggestions []string
179                                         for _, f := range files {
180                                                 name := f.Name()
181                                                 if f.IsDir() {
182                                                         name += "/"
183                                                 }
184                                                 if strings.HasPrefix(name, dirs[len(dirs)-1]) {
185                                                         suggestions = append(suggestions, name)
186                                                 }
187                                         }
188                                         if len(suggestions) == 1 {
189                                                 if len(dirs) > 1 {
190                                                         m.response = strings.Join(dirs[:len(dirs)-1], "/") + "/" + suggestions[0]
191                                                 } else {
192                                                         m.response = suggestions[0]
193                                                 }
194                                                 m.cursorx = Count(m.response)
195                                         }
196                                 }
197                         }
198                 }
199
200                 m.HandleEvent(event, m.history[historyType])
201
202                 if m.cursorx < 0 {
203                         // Cancel
204                         m.hasPrompt = false
205                 }
206         }
207
208         m.Reset()
209         return response, canceled
210 }
211
212 // HandleEvent handles an event for the prompter
213 func (m *Messenger) HandleEvent(event tcell.Event, history []string) {
214         switch e := event.(type) {
215         case *tcell.EventKey:
216                 switch e.Key() {
217                 case tcell.KeyUp:
218                         if m.historyNum > 0 {
219                                 m.historyNum--
220                                 m.response = history[m.historyNum]
221                                 m.cursorx = Count(m.response)
222                         }
223                 case tcell.KeyDown:
224                         if m.historyNum < len(history)-1 {
225                                 m.historyNum++
226                                 m.response = history[m.historyNum]
227                                 m.cursorx = Count(m.response)
228                         }
229                 case tcell.KeyLeft:
230                         if m.cursorx > 0 {
231                                 m.cursorx--
232                         }
233                 case tcell.KeyRight:
234                         if m.cursorx < Count(m.response) {
235                                 m.cursorx++
236                         }
237                 case tcell.KeyBackspace2, tcell.KeyBackspace:
238                         if m.cursorx > 0 {
239                                 m.response = string([]rune(m.response)[:m.cursorx-1]) + string(m.response[m.cursorx:])
240                         }
241                         m.cursorx--
242                 case tcell.KeyRune:
243                         m.response = Insert(m.response, m.cursorx, string(e.Rune()))
244                         m.cursorx++
245                 }
246                 history[m.historyNum] = m.response
247         }
248 }
249
250 // Reset resets the messenger's cursor, message and response
251 func (m *Messenger) Reset() {
252         m.cursorx = 0
253         m.message = ""
254         m.response = ""
255 }
256
257 // Clear clears the line at the bottom of the editor
258 func (m *Messenger) Clear() {
259         w, h := screen.Size()
260         for x := 0; x < w; x++ {
261                 screen.SetContent(x, h-1, ' ', nil, defStyle)
262         }
263 }
264
265 // Display displays messages or prompts
266 func (m *Messenger) Display() {
267         _, h := screen.Size()
268         if m.hasMessage {
269                 runes := []rune(m.message + m.response)
270                 for x := 0; x < len(runes); x++ {
271                         screen.SetContent(x, h-1, runes[x], nil, m.style)
272                 }
273         }
274         if m.hasPrompt {
275                 screen.ShowCursor(Count(m.message)+m.cursorx, h-1)
276                 screen.Show()
277         }
278 }
279
280 // A GutterMessage is a message displayed on the side of the editor
281 type GutterMessage struct {
282         lineNum int
283         msg     string
284         kind    int
285 }
286
287 // These are the different types of messages
288 const (
289         // GutterInfo represents a simple info message
290         GutterInfo = iota
291         // GutterWarning represents a compiler warning
292         GutterWarning
293         // GutterError represents a compiler error
294         GutterError
295 )