]> git.lizzy.rs Git - micro.git/blob - cmd/micro/messenger.go
Fix some issues with unicode handling
[micro.git] / cmd / micro / messenger.go
1 package main
2
3 import (
4         "bufio"
5         "bytes"
6         "fmt"
7         "os"
8         "strconv"
9         "strings"
10
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         fmt.Print("\nPress enter to continue")
28
29         reader := bufio.NewReader(os.Stdin)
30         reader.ReadString('\n')
31
32         if !screenWasNil {
33                 InitScreen()
34         }
35 }
36
37 // TermError sends an error to the user in the terminal. Like TermMessage except formatted
38 // as an error
39 func TermError(filename string, lineNum int, err string) {
40         TermMessage(filename + ", " + strconv.Itoa(lineNum) + ": " + err)
41 }
42
43 // Messenger is an object that makes it easy to send messages to the user
44 // and get input from the user
45 type Messenger struct {
46         // Are we currently prompting the user?
47         hasPrompt bool
48         // Is there a message to print
49         hasMessage bool
50
51         // Message to print
52         message string
53         // The user's response to a prompt
54         response string
55         // style to use when drawing the message
56         style tcell.Style
57
58         // We have to keep track of the cursor for prompting
59         cursorx int
60
61         // This map stores the history for all the different kinds of uses Prompt has
62         // It's a map of history type -> history array
63         history    map[string][]string
64         historyNum int
65
66         // Is the current message a message from the gutter
67         gutterMessage bool
68 }
69
70 // Message sends a message to the user
71 func (m *Messenger) Message(msg ...interface{}) {
72         buf := new(bytes.Buffer)
73         fmt.Fprint(buf, msg...)
74         m.message = buf.String()
75         m.style = defStyle
76
77         if _, ok := colorscheme["message"]; ok {
78                 m.style = colorscheme["message"]
79         }
80         m.hasMessage = true
81 }
82
83 // Error sends an error message to the user
84 func (m *Messenger) Error(msg ...interface{}) {
85         buf := new(bytes.Buffer)
86         fmt.Fprint(buf, msg...)
87         m.message = buf.String()
88         m.style = defStyle.
89                 Foreground(tcell.ColorBlack).
90                 Background(tcell.ColorMaroon)
91
92         if _, ok := colorscheme["error-message"]; ok {
93                 m.style = colorscheme["error-message"]
94         }
95         m.hasMessage = true
96 }
97
98 // YesNoPrompt asks the user a yes or no question (waits for y or n) and returns the result
99 func (m *Messenger) YesNoPrompt(prompt string) (bool, bool) {
100         m.Message(prompt)
101
102         _, h := screen.Size()
103         for {
104                 m.Clear()
105                 m.Display()
106                 screen.ShowCursor(Count(m.message), h-1)
107                 screen.Show()
108                 event := <-events
109
110                 switch e := event.(type) {
111                 case *tcell.EventKey:
112                         switch e.Key() {
113                         case tcell.KeyRune:
114                                 if e.Rune() == 'y' {
115                                         return true, false
116                                 } else if e.Rune() == 'n' {
117                                         return false, false
118                                 }
119                         case tcell.KeyCtrlC, tcell.KeyCtrlQ, tcell.KeyEscape:
120                                 return false, true
121                         }
122                 }
123         }
124 }
125
126 type Completion int
127
128 const (
129         NoCompletion Completion = iota
130         FileCompletion
131         CommandCompletion
132         HelpCompletion
133         OptionCompletion
134 )
135
136 // Prompt sends the user a message and waits for a response to be typed in
137 // This function blocks the main loop while waiting for input
138 func (m *Messenger) Prompt(prompt, historyType string, completionTypes ...Completion) (string, bool) {
139         m.hasPrompt = true
140         m.Message(prompt)
141         if _, ok := m.history[historyType]; !ok {
142                 m.history[historyType] = []string{""}
143         } else {
144                 m.history[historyType] = append(m.history[historyType], "")
145         }
146         m.historyNum = len(m.history[historyType]) - 1
147
148         response, canceled := "", true
149
150         RedrawAll()
151         for m.hasPrompt {
152                 var suggestions []string
153                 m.Clear()
154
155                 event := <-events
156
157                 switch e := event.(type) {
158                 case *tcell.EventKey:
159                         switch e.Key() {
160                         case tcell.KeyCtrlQ, tcell.KeyCtrlC, tcell.KeyEscape:
161                                 // Cancel
162                                 m.hasPrompt = false
163                         case tcell.KeyEnter:
164                                 // User is done entering their response
165                                 m.hasPrompt = false
166                                 response, canceled = m.response, false
167                                 m.history[historyType][len(m.history[historyType])-1] = response
168                         case tcell.KeyTab:
169                                 args := strings.Split(m.response, " ")
170                                 currentArgNum := len(args) - 1
171                                 currentArg := args[currentArgNum]
172                                 var completionType Completion
173
174                                 if completionTypes[0] == CommandCompletion && currentArgNum > 0 {
175                                         if command, ok := commands[args[0]]; ok {
176                                                 completionTypes = append([]Completion{CommandCompletion}, command.completions...)
177                                         }
178                                 }
179
180                                 if currentArgNum >= len(completionTypes) {
181                                         completionType = completionTypes[len(completionTypes)-1]
182                                 } else {
183                                         completionType = completionTypes[currentArgNum]
184                                 }
185
186                                 var chosen string
187                                 if completionType == FileCompletion {
188                                         chosen, suggestions = FileComplete(currentArg)
189                                 } else if completionType == CommandCompletion {
190                                         chosen, suggestions = CommandComplete(currentArg)
191                                 } else if completionType == HelpCompletion {
192                                         chosen, suggestions = HelpComplete(currentArg)
193                                 } else if completionType == OptionCompletion {
194                                         chosen, suggestions = OptionComplete(currentArg)
195                                 }
196
197                                 if chosen != "" {
198                                         if len(args) > 1 {
199                                                 chosen = " " + chosen
200                                         }
201                                         m.response = strings.Join(args[:len(args)-1], " ") + chosen
202                                         m.cursorx = Count(m.response)
203                                 }
204                         }
205                 }
206
207                 m.HandleEvent(event, m.history[historyType])
208
209                 messenger.Clear()
210                 for _, v := range tabs[curTab].views {
211                         v.Display()
212                 }
213                 DisplayTabs()
214                 messenger.Display()
215                 if len(suggestions) > 1 {
216                         m.DisplaySuggestions(suggestions)
217                 }
218                 screen.Show()
219         }
220
221         m.Reset()
222         return response, canceled
223 }
224
225 // HandleEvent handles an event for the prompter
226 func (m *Messenger) HandleEvent(event tcell.Event, history []string) {
227         switch e := event.(type) {
228         case *tcell.EventKey:
229                 switch e.Key() {
230                 case tcell.KeyUp:
231                         if m.historyNum > 0 {
232                                 m.historyNum--
233                                 m.response = history[m.historyNum]
234                                 m.cursorx = Count(m.response)
235                         }
236                 case tcell.KeyDown:
237                         if m.historyNum < len(history)-1 {
238                                 m.historyNum++
239                                 m.response = history[m.historyNum]
240                                 m.cursorx = Count(m.response)
241                         }
242                 case tcell.KeyLeft:
243                         if m.cursorx > 0 {
244                                 m.cursorx--
245                         }
246                 case tcell.KeyRight:
247                         if m.cursorx < Count(m.response) {
248                                 m.cursorx++
249                         }
250                 case tcell.KeyBackspace2, tcell.KeyBackspace:
251                         if m.cursorx > 0 {
252                                 m.response = string([]rune(m.response)[:m.cursorx-1]) + string([]rune(m.response)[m.cursorx:])
253                                 m.cursorx--
254                         }
255                 case tcell.KeyRune:
256                         m.response = Insert(m.response, m.cursorx, string(e.Rune()))
257                         m.cursorx++
258                 }
259                 history[m.historyNum] = m.response
260         }
261 }
262
263 // Reset resets the messenger's cursor, message and response
264 func (m *Messenger) Reset() {
265         m.cursorx = 0
266         m.message = ""
267         m.response = ""
268 }
269
270 // Clear clears the line at the bottom of the editor
271 func (m *Messenger) Clear() {
272         w, h := screen.Size()
273         for x := 0; x < w; x++ {
274                 screen.SetContent(x, h-1, ' ', nil, defStyle)
275         }
276 }
277
278 func (m *Messenger) DisplaySuggestions(suggestions []string) {
279         w, screenH := screen.Size()
280
281         y := screenH - 2
282
283         statusLineStyle := defStyle.Reverse(true)
284         if style, ok := colorscheme["statusline"]; ok {
285                 statusLineStyle = style
286         }
287
288         for x := 0; x < w; x++ {
289                 screen.SetContent(x, y, ' ', nil, statusLineStyle)
290         }
291
292         x := 1
293         for _, suggestion := range suggestions {
294                 for _, c := range suggestion {
295                         screen.SetContent(x, y, c, nil, statusLineStyle)
296                         x++
297                 }
298                 screen.SetContent(x, y, ' ', nil, statusLineStyle)
299                 x++
300         }
301 }
302
303 // Display displays messages or prompts
304 func (m *Messenger) Display() {
305         _, h := screen.Size()
306         if m.hasMessage {
307                 runes := []rune(m.message + m.response)
308                 for x := 0; x < len(runes); x++ {
309                         screen.SetContent(x, h-1, runes[x], nil, m.style)
310                 }
311         }
312         if m.hasPrompt {
313                 screen.ShowCursor(Count(m.message)+m.cursorx, h-1)
314                 screen.Show()
315         }
316 }
317
318 // A GutterMessage is a message displayed on the side of the editor
319 type GutterMessage struct {
320         lineNum int
321         msg     string
322         kind    int
323 }
324
325 // These are the different types of messages
326 const (
327         // GutterInfo represents a simple info message
328         GutterInfo = iota
329         // GutterWarning represents a compiler warning
330         GutterWarning
331         // GutterError represents a compiler error
332         GutterError
333 )