11 "github.com/mattn/go-runewidth"
12 "github.com/zyedidia/clipboard"
13 "github.com/zyedidia/tcell"
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
30 fmt.Print("\nPress enter to continue")
32 reader := bufio.NewReader(os.Stdin)
33 reader.ReadString('\n')
40 // TermError sends an error to the user in the terminal. Like TermMessage except formatted
42 func TermError(filename string, lineNum int, err string) {
43 TermMessage(filename + ", " + strconv.Itoa(lineNum) + ": " + err)
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 {
50 // Are we currently prompting the user?
52 // Is there a message to print
57 // The user's response to a prompt
59 // style to use when drawing the message
62 // We have to keep track of the cursor for prompting
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
70 // Is the current message a message from the gutter
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()
83 func (m *Messenger) getBuffer() *Buffer {
85 m.log = NewBufferFromString("", "")
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
102 if _, ok := colorscheme["message"]; ok {
103 m.style = colorscheme["message"]
108 // add the message to the log regardless of active prompts
109 m.AddLog(displayMessage)
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...)
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()
123 Foreground(tcell.ColorBlack).
124 Background(tcell.ColorMaroon)
126 if _, ok := colorscheme["error-message"]; ok {
127 m.style = colorscheme["error-message"]
131 // add the message to the log regardless of active prompts
132 m.AddLog(buf.String())
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
142 if _, ok := colorscheme["message"]; ok {
143 m.style = colorscheme["message"]
147 // add the message to the log regardless of active prompts
148 m.AddLog(displayMessage)
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) {
156 _, h := screen.Size()
160 screen.ShowCursor(Count(m.message), h-1)
164 switch e := event.(type) {
165 case *tcell.EventKey:
168 if e.Rune() == 'y' || e.Rune() == 'Y' {
172 } else if e.Rune() == 'n' || e.Rune() == 'N' {
177 case tcell.KeyCtrlC, tcell.KeyCtrlQ, tcell.KeyEscape:
178 m.AddLog("\t--> (cancel)")
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) {
193 _, h := screen.Size()
197 screen.ShowCursor(Count(m.message), h-1)
201 switch e := event.(type) {
202 case *tcell.EventKey:
205 for _, r := range responses {
207 m.AddLog("\t--> " + string(r))
214 case tcell.KeyCtrlC, tcell.KeyCtrlQ, tcell.KeyEscape:
215 m.AddLog("\t--> (cancel)")
228 NoCompletion Completion = iota
235 OptionValueCompletion
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) {
243 if _, ok := m.history[historyType]; !ok {
244 m.history[historyType] = []string{""}
246 m.history[historyType] = append(m.history[historyType], "")
248 m.historyNum = len(m.history[historyType]) - 1
250 response, canceled := placeholder, true
251 m.response = response
252 m.cursorx = Count(placeholder)
256 var suggestions []string
261 switch e := event.(type) {
262 case *tcell.EventKey:
264 case tcell.KeyCtrlQ, tcell.KeyCtrlC, tcell.KeyEscape:
266 m.AddLog("\t--> (cancel)")
269 // User is done entering their response
270 m.AddLog("\t--> " + m.response)
272 response, canceled = m.response, false
273 m.history[historyType][len(m.history[historyType])-1] = response
275 args := SplitCommandArgs(m.response)
276 currentArgNum := len(args) - 1
277 currentArg := args[currentArgNum]
278 var completionType Completion
280 if completionTypes[0] == CommandCompletion && currentArgNum > 0 {
281 if command, ok := commands[args[0]]; ok {
282 completionTypes = append([]Completion{CommandCompletion}, command.completions...)
286 if currentArgNum >= len(completionTypes) {
287 completionType = completionTypes[len(completionTypes)-1]
289 completionType = completionTypes[currentArgNum]
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)
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)
313 if len(suggestions) > 1 {
314 chosen = chosen + CommonSubstring(suggestions...)
318 m.response = JoinCommandArgs(append(args[:len(args)-1], chosen)...)
319 m.cursorx = Count(m.response)
324 m.HandleEvent(event, m.history[historyType])
327 for _, v := range tabs[curTab].views {
332 if len(suggestions) > 1 {
333 m.DisplaySuggestions(suggestions)
340 return response, canceled
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 {
355 if e.Modifiers() == key.modifiers {
356 for _, action := range actions {
357 funcName := FuncName(action)
359 case "main.(*View).CursorUp":
360 if m.historyNum > 0 {
362 m.response = history[m.historyNum]
363 m.cursorx = Count(m.response)
365 case "main.(*View).CursorDown":
366 if m.historyNum < len(history)-1 {
368 m.response = history[m.historyNum]
369 m.cursorx = Count(m.response)
371 case "main.(*View).CursorLeft":
375 case "main.(*View).CursorRight":
376 if m.cursorx < Count(m.response) {
379 case "main.(*View).CursorStart", "main.(*View).StartOfLine":
381 case "main.(*View).CursorEnd", "main.(*View).EndOfLine":
382 m.cursorx = Count(m.response)
383 case "main.(*View).Backspace":
385 m.response = string([]rune(m.response)[:m.cursorx-1]) + string([]rune(m.response)[m.cursorx:])
388 case "main.(*View).Paste":
389 clip, _ := clipboard.ReadAll("clipboard")
390 m.response = Insert(m.response, m.cursorx, clip)
391 m.cursorx += Count(clip)
400 m.response = Insert(m.response, m.cursorx, string(e.Rune()))
403 history[m.historyNum] = m.response
405 case *tcell.EventPaste:
407 m.response = Insert(m.response, m.cursorx, clip)
408 m.cursorx += Count(clip)
409 case *tcell.EventMouse:
411 x -= Count(m.message)
412 button := e.Buttons()
413 _, screenH := screen.Size()
421 } else if m.cursorx > Count(m.response) {
422 m.cursorx = Count(m.response)
429 // Reset resets the messenger's cursor, message and response
430 func (m *Messenger) Reset() {
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)
444 func (m *Messenger) DisplaySuggestions(suggestions []string) {
445 w, screenH := screen.Size()
449 statusLineStyle := defStyle.Reverse(true)
450 if style, ok := colorscheme["statusline"]; ok {
451 statusLineStyle = style
454 for x := 0; x < w; x++ {
455 screen.SetContent(x, y, ' ', nil, statusLineStyle)
459 for _, suggestion := range suggestions {
460 for _, c := range suggestion {
461 screen.SetContent(x, y, c, nil, statusLineStyle)
464 screen.SetContent(x, y, ' ', nil, statusLineStyle)
469 // Display displays messages or prompts
470 func (m *Messenger) Display() {
471 _, h := screen.Size()
473 if m.hasPrompt || globalSettings["infobar"].(bool) {
474 runes := []rune(m.message + m.response)
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])
484 screen.ShowCursor(Count(m.message)+m.cursorx, h-1)
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
497 decoder := gob.NewDecoder(file)
498 err = decoder.Decode(&decodedMap)
502 m.Error("Error loading history:", err)
507 m.history = decodedMap
509 m.history = make(map[string][]string)
513 // SaveHistory saves the user's command history to configDir/buffers/history
514 // only if the savehistory option is on
515 func (m *Messenger) SaveHistory() {
516 if GetGlobalOption("savehistory").(bool) {
517 // Don't save history past 100
518 for k, v := range m.history {
520 m.history[k] = v[0:100]
524 file, err := os.Create(configDir + "/buffers/history")
526 encoder := gob.NewEncoder(file)
528 err = encoder.Encode(m.history)
530 m.Error("Error saving history:", err)
538 // A GutterMessage is a message displayed on the side of the editor
539 type GutterMessage struct {
545 // These are the different types of messages
547 // GutterInfo represents a simple info message
549 // GutterWarning represents a compiler warning
551 // GutterError represents a compiler error