11 "github.com/mattn/go-runewidth"
12 "github.com/zyedidia/clipboard"
13 "github.com/zyedidia/micro/cmd/micro/shellwords"
14 "github.com/zyedidia/tcell"
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
31 fmt.Print("\nPress enter to continue")
33 reader := bufio.NewReader(os.Stdin)
34 reader.ReadString('\n')
41 // TermError sends an error to the user in the terminal. Like TermMessage except formatted
43 func TermError(filename string, lineNum int, err string) {
44 TermMessage(filename + ", " + strconv.Itoa(lineNum) + ": " + err)
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 {
51 // Are we currently prompting the user?
53 // Is there a message to print
58 // The user's response to a prompt
60 // style to use when drawing the message
63 // We have to keep track of the cursor for prompting
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
71 // Is the current message a message from the gutter
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()
84 func (m *Messenger) getBuffer() *Buffer {
86 m.log = NewBufferFromString("", "")
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
103 if _, ok := colorscheme["message"]; ok {
104 m.style = colorscheme["message"]
109 // add the message to the log regardless of active prompts
110 m.AddLog(displayMessage)
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...)
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()
124 Foreground(tcell.ColorBlack).
125 Background(tcell.ColorMaroon)
127 if _, ok := colorscheme["error-message"]; ok {
128 m.style = colorscheme["error-message"]
132 // add the message to the log regardless of active prompts
133 m.AddLog(buf.String())
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
143 if _, ok := colorscheme["message"]; ok {
144 m.style = colorscheme["message"]
148 // add the message to the log regardless of active prompts
149 m.AddLog(displayMessage)
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) {
157 _, h := screen.Size()
161 screen.ShowCursor(Count(m.message), h-1)
165 switch e := event.(type) {
166 case *tcell.EventKey:
169 if e.Rune() == 'y' || e.Rune() == 'Y' {
173 } else if e.Rune() == 'n' || e.Rune() == 'N' {
178 case tcell.KeyCtrlC, tcell.KeyCtrlQ, tcell.KeyEscape:
179 m.AddLog("\t--> (cancel)")
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) {
194 _, h := screen.Size()
198 screen.ShowCursor(Count(m.message), h-1)
202 switch e := event.(type) {
203 case *tcell.EventKey:
206 for _, r := range responses {
208 m.AddLog("\t--> " + string(r))
215 case tcell.KeyCtrlC, tcell.KeyCtrlQ, tcell.KeyEscape:
216 m.AddLog("\t--> (cancel)")
226 // Completion represents a type of completion
230 NoCompletion Completion = iota
237 OptionValueCompletion
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) {
245 if _, ok := m.history[historyType]; !ok {
246 m.history[historyType] = []string{""}
248 m.history[historyType] = append(m.history[historyType], "")
250 m.historyNum = len(m.history[historyType]) - 1
252 response, canceled := placeholder, true
253 m.response = response
254 m.cursorx = Count(placeholder)
258 var suggestions []string
263 switch e := event.(type) {
264 case *tcell.EventResize:
265 for _, t := range tabs {
269 case *tcell.EventKey:
271 case tcell.KeyCtrlQ, tcell.KeyCtrlC, tcell.KeyEscape:
273 m.AddLog("\t--> (cancel)")
276 // User is done entering their response
277 m.AddLog("\t--> " + m.response)
279 response, canceled = m.response, false
280 m.history[historyType][len(m.history[historyType])-1] = response
282 args, err := shellwords.Split(m.response)
289 currentArgNum = len(args) - 1
290 currentArg = args[currentArgNum]
292 var completionType Completion
294 if completionTypes[0] == CommandCompletion && currentArgNum > 0 {
295 if command, ok := commands[args[0]]; ok {
296 completionTypes = append([]Completion{CommandCompletion}, command.completions...)
300 if currentArgNum >= len(completionTypes) {
301 completionType = completionTypes[len(completionTypes)-1]
303 completionType = completionTypes[currentArgNum]
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)
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)
327 if len(suggestions) > 1 {
328 chosen = chosen + CommonSubstring(suggestions...)
331 if len(suggestions) != 0 && chosen != "" {
332 m.response = shellwords.Join(append(args[:len(args)-1], chosen)...)
333 m.cursorx = Count(m.response)
338 m.HandleEvent(event, m.history[historyType])
341 for _, v := range tabs[curTab].Views {
346 if len(suggestions) > 1 {
347 m.DisplaySuggestions(suggestions)
354 return response, canceled
357 // UpHistory fetches the previous item in the history
358 func (m *Messenger) UpHistory(history []string) {
359 if m.historyNum > 0 {
361 m.response = history[m.historyNum]
362 m.cursorx = Count(m.response)
366 // DownHistory fetches the next item in the history
367 func (m *Messenger) DownHistory(history []string) {
368 if m.historyNum < len(history)-1 {
370 m.response = history[m.historyNum]
371 m.cursorx = Count(m.response)
375 // CursorLeft moves the cursor one character left
376 func (m *Messenger) CursorLeft() {
382 // CursorRight moves the cursor one character right
383 func (m *Messenger) CursorRight() {
384 if m.cursorx < Count(m.response) {
389 // Start moves the cursor to the start of the line
390 func (m *Messenger) Start() {
394 // End moves the cursor to the end of the line
395 func (m *Messenger) End() {
396 m.cursorx = Count(m.response)
399 // Backspace deletes one character
400 func (m *Messenger) Backspace() {
402 m.response = string([]rune(m.response)[:m.cursorx-1]) + string([]rune(m.response)[m.cursorx:])
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)
414 // WordLeft moves the cursor one word to the left
415 func (m *Messenger) WordLeft() {
416 response := []rune(m.response)
421 for IsWhitespace(response[m.cursorx]) {
428 for IsWordChar(string(response[m.cursorx])) {
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) {
443 for IsWhitespace(response[m.cursorx]) {
445 if m.cursorx >= len(response) {
451 if m.cursorx >= len(response) {
454 for IsWordChar(string(response[m.cursorx])) {
456 if m.cursorx >= len(response) {
462 // DeleteWordLeft deletes one word to the left
463 func (m *Messenger) DeleteWordLeft() {
465 m.response = string([]rune(m.response)[:m.cursorx])
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:
480 m.DownHistory(history)
482 if e.Modifiers() == tcell.ModCtrl {
484 } else if e.Modifiers() == tcell.ModAlt || e.Modifiers() == tcell.ModMeta {
490 if e.Modifiers() == tcell.ModCtrl {
492 } else if e.Modifiers() == tcell.ModAlt || e.Modifiers() == tcell.ModMeta {
497 case tcell.KeyBackspace2, tcell.KeyBackspace:
498 if e.Modifiers() == tcell.ModCtrl || e.Modifiers() == tcell.ModAlt || e.Modifiers() == tcell.ModMeta {
512 m.response = Insert(m.response, m.cursorx, string(e.Rune()))
515 history[m.historyNum] = m.response
517 case *tcell.EventPaste:
519 m.response = Insert(m.response, m.cursorx, clip)
520 m.cursorx += Count(clip)
521 case *tcell.EventMouse:
523 x -= Count(m.message)
524 button := e.Buttons()
525 _, screenH := screen.Size()
533 } else if m.cursorx > Count(m.response) {
534 m.cursorx = Count(m.response)
541 // Reset resets the messenger's cursor, message and response
542 func (m *Messenger) Reset() {
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)
556 func (m *Messenger) DisplaySuggestions(suggestions []string) {
557 w, screenH := screen.Size()
561 statusLineStyle := defStyle.Reverse(true)
562 if style, ok := colorscheme["statusline"]; ok {
563 statusLineStyle = style
566 for x := 0; x < w; x++ {
567 screen.SetContent(x, y, ' ', nil, statusLineStyle)
571 for _, suggestion := range suggestions {
572 for _, c := range suggestion {
573 screen.SetContent(x, y, c, nil, statusLineStyle)
576 screen.SetContent(x, y, ' ', nil, statusLineStyle)
581 // Display displays messages or prompts
582 func (m *Messenger) Display() {
583 _, h := screen.Size()
585 if m.hasPrompt || globalSettings["infobar"].(bool) {
586 runes := []rune(m.message + m.response)
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])
596 screen.ShowCursor(Count(m.message)+m.cursorx, h-1)
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")
608 var decodedMap map[string][]string
610 decoder := gob.NewDecoder(file)
611 err = decoder.Decode(&decodedMap)
614 m.Error("Error loading history:", err)
619 if decodedMap != nil {
620 m.history = decodedMap
622 m.history = make(map[string][]string)
625 m.history = make(map[string][]string)
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 {
636 m.history[k] = v[len(m.history[k])-100:]
640 file, err := os.Create(configDir + "/buffers/history")
643 encoder := gob.NewEncoder(file)
645 err = encoder.Encode(m.history)
647 m.Error("Error saving history:", err)
654 // A GutterMessage is a message displayed on the side of the editor
655 type GutterMessage struct {
661 // These are the different types of messages
663 // GutterInfo represents a simple info message
665 // GutterWarning represents a compiler warning
667 // GutterError represents a compiler error