14 shellquote "github.com/kballard/go-shellquote"
15 "github.com/zyedidia/micro/v2/internal/buffer"
16 "github.com/zyedidia/micro/v2/internal/clipboard"
17 "github.com/zyedidia/micro/v2/internal/config"
18 "github.com/zyedidia/micro/v2/internal/screen"
19 "github.com/zyedidia/micro/v2/internal/shell"
20 "github.com/zyedidia/micro/v2/internal/util"
23 // A Command contains information about how to execute a command
24 // It has the action for that command as well as a completer function
26 action func(*BufPane, []string)
27 completer buffer.Completer
30 var commands map[string]Command
33 commands = map[string]Command{
34 "set": {(*BufPane).SetCmd, OptionValueComplete},
35 "reset": {(*BufPane).ResetCmd, OptionValueComplete},
36 "setlocal": {(*BufPane).SetLocalCmd, OptionValueComplete},
37 "show": {(*BufPane).ShowCmd, OptionComplete},
38 "showkey": {(*BufPane).ShowKeyCmd, nil},
39 "run": {(*BufPane).RunCmd, nil},
40 "bind": {(*BufPane).BindCmd, nil},
41 "unbind": {(*BufPane).UnbindCmd, nil},
42 "quit": {(*BufPane).QuitCmd, nil},
43 "goto": {(*BufPane).GotoCmd, nil},
44 "save": {(*BufPane).SaveCmd, nil},
45 "replace": {(*BufPane).ReplaceCmd, nil},
46 "replaceall": {(*BufPane).ReplaceAllCmd, nil},
47 "vsplit": {(*BufPane).VSplitCmd, buffer.FileComplete},
48 "hsplit": {(*BufPane).HSplitCmd, buffer.FileComplete},
49 "tab": {(*BufPane).NewTabCmd, buffer.FileComplete},
50 "help": {(*BufPane).HelpCmd, HelpComplete},
51 "eval": {(*BufPane).EvalCmd, nil},
52 "log": {(*BufPane).ToggleLogCmd, nil},
53 "plugin": {(*BufPane).PluginCmd, PluginComplete},
54 "reload": {(*BufPane).ReloadCmd, nil},
55 "reopen": {(*BufPane).ReopenCmd, nil},
56 "cd": {(*BufPane).CdCmd, buffer.FileComplete},
57 "pwd": {(*BufPane).PwdCmd, nil},
58 "open": {(*BufPane).OpenCmd, buffer.FileComplete},
59 "tabmove": {(*BufPane).TabMoveCmd, nil},
60 "tabswitch": {(*BufPane).TabSwitchCmd, nil},
61 "term": {(*BufPane).TermCmd, nil},
62 "memusage": {(*BufPane).MemUsageCmd, nil},
63 "retab": {(*BufPane).RetabCmd, nil},
64 "raw": {(*BufPane).RawCmd, nil},
65 "textfilter": {(*BufPane).TextFilterCmd, nil},
69 // MakeCommand is a function to easily create new commands
70 // This can be called by plugins in Lua so that plugins can define their own commands
71 func MakeCommand(name string, action func(bp *BufPane, args []string), completer buffer.Completer) {
73 commands[name] = Command{action, completer}
77 // CommandEditAction returns a bindable function that opens a prompt with
78 // the given string and executes the command when the user presses
80 func CommandEditAction(prompt string) BufKeyAction {
81 return func(h *BufPane) bool {
82 InfoBar.Prompt("> ", prompt, "Command", nil, func(resp string, canceled bool) {
84 MainTab().CurPane().HandleCommand(resp)
91 // CommandAction returns a bindable function which executes the
93 func CommandAction(cmd string) BufKeyAction {
94 return func(h *BufPane) bool {
95 MainTab().CurPane().HandleCommand(cmd)
100 var PluginCmds = []string{"install", "remove", "update", "available", "list", "search"}
102 // PluginCmd installs, removes, updates, lists, or searches for given plugins
103 func (h *BufPane) PluginCmd(args []string) {
105 InfoBar.Error("Not enough arguments")
109 if h.Buf.Type != buffer.BTLog {
113 config.PluginCommand(buffer.LogBuf, args[0], args[1:])
116 // RetabCmd changes all spaces to tabs or all tabs to spaces
117 // depending on the user's settings
118 func (h *BufPane) RetabCmd(args []string) {
122 // RawCmd opens a new raw view which displays the escape sequences micro
123 // is receiving in real-time
124 func (h *BufPane) RawCmd(args []string) {
125 width, height := screen.Screen.Size()
126 iOffset := config.GetInfoBarOffset()
127 tp := NewTabFromPane(0, 0, width, height-iOffset, NewRawPane(nil))
129 Tabs.SetActive(len(Tabs.List) - 1)
132 // TextFilterCmd filters the selection through the command.
133 // Selection goes to the command input.
134 // On successful run command output replaces the current selection.
135 func (h *BufPane) TextFilterCmd(args []string) {
137 InfoBar.Error("usage: textfilter arguments")
140 sel := h.Cursor.GetSelection()
142 h.Cursor.SelectWord()
143 sel = h.Cursor.GetSelection()
145 var bout, berr bytes.Buffer
146 cmd := exec.Command(args[0], args[1:]...)
147 cmd.Stdin = strings.NewReader(string(sel))
152 InfoBar.Error(err.Error() + " " + berr.String())
155 h.Cursor.DeleteSelection()
156 h.Buf.Insert(h.Cursor.Loc, bout.String())
159 // TabMoveCmd moves the current tab to a given index (starts at 1). The
160 // displaced tabs are moved up.
161 func (h *BufPane) TabMoveCmd(args []string) {
163 InfoBar.Error("Not enough arguments: provide an index, starting at 1")
167 if len(args[0]) <= 0 {
168 InfoBar.Error("Invalid argument: empty string")
172 num, err := strconv.Atoi(args[0])
174 InfoBar.Error("Invalid argument: ", err)
178 // Preserve sign for relative move, if one exists
179 var shiftDirection byte
180 if strings.Contains("-+", string([]byte{args[0][0]})) {
181 shiftDirection = args[0][0]
184 // Relative positions -> absolute positions
185 idxFrom := Tabs.Active()
187 offset := util.Abs(num)
188 if shiftDirection == '-' {
189 idxTo = idxFrom - offset
190 } else if shiftDirection == '+' {
191 idxTo = idxFrom + offset
196 // Restrain position to within the valid range
197 idxTo = util.Clamp(idxTo, 0, len(Tabs.List)-1)
199 activeTab := Tabs.List[idxFrom]
200 Tabs.RemoveTab(activeTab.ID())
201 Tabs.List = append(Tabs.List, nil)
202 copy(Tabs.List[idxTo+1:], Tabs.List[idxTo:])
203 Tabs.List[idxTo] = activeTab
205 Tabs.SetActive(idxTo)
206 // InfoBar.Message(fmt.Sprintf("Moved tab from slot %d to %d", idxFrom+1, idxTo+1))
209 // TabSwitchCmd switches to a given tab either by name or by number
210 func (h *BufPane) TabSwitchCmd(args []string) {
212 num, err := strconv.Atoi(args[0])
214 // Check for tab with this name
217 for i, t := range Tabs.List {
218 if t.Panes[t.active].Name() == args[0] {
224 InfoBar.Error("Could not find tab: ", err)
228 if num >= 0 && num < len(Tabs.List) {
231 InfoBar.Error("Invalid tab index")
237 // CdCmd changes the current working directory
238 func (h *BufPane) CdCmd(args []string) {
240 path, err := util.ReplaceHome(args[0])
251 for _, b := range buffer.OpenBuffers {
253 b.Path, _ = util.MakeRelative(b.AbsPath, wd)
254 if p, _ := filepath.Abs(b.Path); !strings.Contains(p, wd) {
262 // MemUsageCmd prints micro's memory usage
263 // Alloc shows how many bytes are currently in use
264 // Sys shows how many bytes have been requested from the operating system
265 // NumGC shows how many times the GC has been run
266 // Note that Go commonly reserves more memory from the OS than is currently in-use/required
267 // Additionally, even if Go returns memory to the OS, the OS does not always claim it because
268 // there may be plenty of memory to spare
269 func (h *BufPane) MemUsageCmd(args []string) {
270 InfoBar.Message(util.GetMemStats())
273 // PwdCmd prints the current working directory
274 func (h *BufPane) PwdCmd(args []string) {
275 wd, err := os.Getwd()
277 InfoBar.Message(err.Error())
283 // OpenCmd opens a new buffer with a given filename
284 func (h *BufPane) OpenCmd(args []string) {
287 // the filename might or might not be quoted, so unquote first then join the strings.
288 args, err := shellquote.Split(filename)
290 InfoBar.Error("Error parsing args ", err)
296 filename = strings.Join(args, " ")
299 b, err := buffer.NewBufferFromFile(filename, buffer.BTDefault)
306 if h.Buf.Modified() {
307 InfoBar.YNPrompt("Save changes to "+h.Buf.GetName()+" before closing? (y,n,esc)", func(yes, canceled bool) {
308 if !canceled && !yes {
310 } else if !canceled && yes {
319 InfoBar.Error("No filename")
323 // ToggleLogCmd toggles the log view
324 func (h *BufPane) ToggleLogCmd(args []string) {
325 if h.Buf.Type != buffer.BTLog {
332 // ReloadCmd reloads all files (syntax files, colorschemes...)
333 func (h *BufPane) ReloadCmd(args []string) {
337 func ReloadConfig() {
338 config.InitRuntimeFiles()
339 err := config.ReadSettings()
341 screen.TermMessage(err)
343 err = config.InitGlobalSettings()
345 screen.TermMessage(err)
350 err = config.InitColorscheme()
352 screen.TermMessage(err)
355 for _, b := range buffer.OpenBuffers {
360 // ReopenCmd reopens the buffer (reload from disk)
361 func (h *BufPane) ReopenCmd(args []string) {
362 if h.Buf.Modified() {
363 InfoBar.YNPrompt("Save file before reopen?", func(yes, canceled bool) {
364 if !canceled && yes {
367 } else if !canceled {
376 func (h *BufPane) openHelp(page string) error {
377 if data, err := config.FindRuntimeFile(config.RTHelp, page).Data(); err != nil {
378 return errors.New(fmt.Sprint("Unable to load help text", page, "\n", err))
380 helpBuffer := buffer.NewBufferFromString(string(data), page+".md", buffer.BTHelp)
381 helpBuffer.SetName("Help " + page)
383 if h.Buf.Type == buffer.BTHelp {
384 h.OpenBuffer(helpBuffer)
386 h.HSplitBuf(helpBuffer)
392 // HelpCmd tries to open the given help page in a horizontal split
393 func (h *BufPane) HelpCmd(args []string) {
395 // Open the default help if the user just typed "> help"
398 if config.FindRuntimeFile(config.RTHelp, args[0]) != nil {
399 err := h.openHelp(args[0])
404 InfoBar.Error("Sorry, no help for ", args[0])
409 // VSplitCmd opens a vertical split with file given in the first argument
410 // If no file is given, it opens an empty buffer in a new split
411 func (h *BufPane) VSplitCmd(args []string) {
413 // Open an empty vertical split
418 buf, err := buffer.NewBufferFromFile(args[0], buffer.BTDefault)
427 // HSplitCmd opens a horizontal split with file given in the first argument
428 // If no file is given, it opens an empty buffer in a new split
429 func (h *BufPane) HSplitCmd(args []string) {
431 // Open an empty horizontal split
436 buf, err := buffer.NewBufferFromFile(args[0], buffer.BTDefault)
445 // EvalCmd evaluates a lua expression
446 func (h *BufPane) EvalCmd(args []string) {
447 InfoBar.Error("Eval unsupported")
450 // NewTabCmd opens the given file in a new tab
451 func (h *BufPane) NewTabCmd(args []string) {
452 width, height := screen.Screen.Size()
453 iOffset := config.GetInfoBarOffset()
455 for _, a := range args {
456 b, err := buffer.NewBufferFromFile(a, buffer.BTDefault)
461 tp := NewTabFromBuffer(0, 0, width, height-1-iOffset, b)
463 Tabs.SetActive(len(Tabs.List) - 1)
466 b := buffer.NewBufferFromString("", "", buffer.BTDefault)
467 tp := NewTabFromBuffer(0, 0, width, height-iOffset, b)
469 Tabs.SetActive(len(Tabs.List) - 1)
473 func SetGlobalOptionNative(option string, nativeValue interface{}) error {
475 for _, s := range config.LocalSettings {
483 config.GlobalSettings[option] = nativeValue
484 config.ModifiedSettings[option] = true
486 if option == "colorscheme" {
488 config.InitColorscheme()
489 for _, b := range buffer.OpenBuffers {
492 } else if option == "infobar" || option == "keymenu" {
494 } else if option == "mouse" {
495 if !nativeValue.(bool) {
496 screen.Screen.DisableMouse()
498 screen.Screen.EnableMouse()
500 } else if option == "autosave" {
501 if nativeValue.(float64) > 0 {
502 config.SetAutoTime(int(nativeValue.(float64)))
503 config.StartAutoSave()
505 config.SetAutoTime(0)
507 } else if option == "paste" {
508 screen.Screen.SetPaste(nativeValue.(bool))
509 } else if option == "clipboard" {
510 m := clipboard.SetMethod(nativeValue.(string))
511 err := clipboard.Initialize(m)
516 for _, pl := range config.Plugins {
517 if option == pl.Name {
518 if nativeValue.(bool) && !pl.Loaded {
520 _, err := pl.Call("init")
521 if err != nil && err != config.ErrNoSuchFunction {
522 screen.TermMessage(err)
524 } else if !nativeValue.(bool) && pl.Loaded {
525 _, err := pl.Call("deinit")
526 if err != nil && err != config.ErrNoSuchFunction {
527 screen.TermMessage(err)
535 for _, b := range buffer.OpenBuffers {
536 b.SetOptionNative(option, nativeValue)
539 return config.WriteSettings(filepath.Join(config.ConfigDir, "settings.json"))
542 func SetGlobalOption(option, value string) error {
543 if _, ok := config.GlobalSettings[option]; !ok {
544 return config.ErrInvalidOption
547 nativeValue, err := config.GetNativeValue(option, config.GlobalSettings[option], value)
552 return SetGlobalOptionNative(option, nativeValue)
555 // ResetCmd resets a setting to its default value
556 func (h *BufPane) ResetCmd(args []string) {
558 InfoBar.Error("Not enough arguments")
564 defaultGlobals := config.DefaultGlobalSettings()
565 defaultLocals := config.DefaultCommonSettings()
567 if _, ok := defaultGlobals[option]; ok {
568 SetGlobalOptionNative(option, defaultGlobals[option])
571 if _, ok := defaultLocals[option]; ok {
572 h.Buf.SetOptionNative(option, defaultLocals[option])
575 InfoBar.Error(config.ErrInvalidOption)
578 // SetCmd sets an option
579 func (h *BufPane) SetCmd(args []string) {
581 InfoBar.Error("Not enough arguments")
588 err := SetGlobalOption(option, value)
589 if err == config.ErrInvalidOption {
590 err := h.Buf.SetOption(option, value)
594 } else if err != nil {
599 // SetLocalCmd sets an option local to the buffer
600 func (h *BufPane) SetLocalCmd(args []string) {
602 InfoBar.Error("Not enough arguments")
609 err := h.Buf.SetOption(option, value)
615 // ShowCmd shows the value of the given option
616 func (h *BufPane) ShowCmd(args []string) {
618 InfoBar.Error("Please provide an option to show")
622 var option interface{}
623 if opt, ok := h.Buf.Settings[args[0]]; ok {
625 } else if opt, ok := config.GlobalSettings[args[0]]; ok {
630 InfoBar.Error(args[0], " is not a valid option")
634 InfoBar.Message(option)
637 // ShowKeyCmd displays the action that a key is bound to
638 func (h *BufPane) ShowKeyCmd(args []string) {
640 InfoBar.Error("Please provide a key to show")
644 event, err := findEvent(args[0])
649 if action, ok := config.Bindings["buffer"][event.Name()]; ok {
650 InfoBar.Message(action)
652 InfoBar.Message(args[0], " has no binding")
656 // BindCmd creates a new keybinding
657 func (h *BufPane) BindCmd(args []string) {
659 InfoBar.Error("Not enough arguments")
663 _, err := TryBindKey(args[0], args[1], true)
669 // UnbindCmd binds a key to its default action
670 func (h *BufPane) UnbindCmd(args []string) {
672 InfoBar.Error("Not enough arguments")
676 err := UnbindKey(args[0])
682 // RunCmd runs a shell command in the background
683 func (h *BufPane) RunCmd(args []string) {
684 runf, err := shell.RunBackgroundShell(shellquote.Join(args...))
689 InfoBar.Message(runf())
695 // QuitCmd closes the main view
696 func (h *BufPane) QuitCmd(args []string) {
700 // GotoCmd is a command that will send the cursor to a certain
701 // position in the buffer
702 // For example: `goto line`, or `goto line:col`
703 func (h *BufPane) GotoCmd(args []string) {
705 InfoBar.Error("Not enough arguments")
707 h.RemoveAllMultiCursors()
708 if strings.Contains(args[0], ":") {
709 parts := strings.SplitN(args[0], ":", 2)
710 line, err := strconv.Atoi(parts[0])
715 col, err := strconv.Atoi(parts[1])
721 line = h.Buf.LinesNum() + 1 + line
723 line = util.Clamp(line-1, 0, h.Buf.LinesNum()-1)
724 col = util.Clamp(col-1, 0, util.CharacterCount(h.Buf.LineBytes(line)))
725 h.Cursor.GotoLoc(buffer.Loc{col, line})
727 line, err := strconv.Atoi(args[0])
733 line = h.Buf.LinesNum() + 1 + line
735 line = util.Clamp(line-1, 0, h.Buf.LinesNum()-1)
736 h.Cursor.GotoLoc(buffer.Loc{0, line})
742 // SaveCmd saves the buffer optionally with an argument file name
743 func (h *BufPane) SaveCmd(args []string) {
747 h.Buf.SaveAs(args[0])
751 // ReplaceCmd runs search and replace
752 func (h *BufPane) ReplaceCmd(args []string) {
753 if len(args) < 2 || len(args) > 4 {
754 // We need to find both a search and replace expression
755 InfoBar.Error("Invalid replace statement: " + strings.Join(args, " "))
763 foundReplace := false
765 var replaceStr string
766 for _, arg := range args {
776 } else if !foundReplace {
780 InfoBar.Error("Invalid flag: " + arg)
787 search = regexp.QuoteMeta(search)
790 replace := []byte(replaceStr)
792 var regex *regexp.Regexp
794 if h.Buf.Settings["ignorecase"].(bool) {
795 regex, err = regexp.Compile("(?im)" + search)
797 regex, err = regexp.Compile("(?m)" + search)
800 // There was an error with the user's regex
806 start := h.Buf.Start()
808 selection := h.Cursor.HasSelection()
810 start = h.Cursor.CurSelection[0]
811 end = h.Cursor.CurSelection[1]
814 nreplaced, _ = h.Buf.ReplaceRegex(start, end, regex, replace)
816 inRange := func(l buffer.Loc) bool {
817 return l.GreaterEqual(start) && l.LessEqual(end)
820 searchLoc := h.Cursor.Loc
821 var doReplacement func()
822 doReplacement = func() {
823 locs, found, err := h.Buf.FindNext(search, start, end, searchLoc, true, true)
828 if !found || !inRange(locs[0]) || !inRange(locs[1]) {
829 h.Cursor.ResetSelection()
830 h.Buf.RelocateCursors()
835 h.Cursor.SetSelectionStart(locs[0])
836 h.Cursor.SetSelectionEnd(locs[1])
837 h.Cursor.GotoLoc(locs[0])
841 InfoBar.YNPrompt("Perform replacement (y,n,esc)", func(yes, canceled bool) {
842 if !canceled && yes {
843 _, nrunes := h.Buf.ReplaceRegex(locs[0], locs[1], regex, replace)
846 searchLoc.X += nrunes + locs[0].Diff(locs[1], h.Buf)
847 if end.Y == locs[1].Y {
848 end = end.Move(nrunes, h.Buf)
850 h.Cursor.Loc = searchLoc
852 } else if !canceled && !yes {
854 searchLoc.X += util.CharacterCount(replace)
856 h.Cursor.ResetSelection()
857 h.Buf.RelocateCursors()
866 h.Buf.RelocateCursors()
871 s = fmt.Sprintf("Replaced %d occurrences of %s", nreplaced, search)
872 } else if nreplaced == 1 {
873 s = fmt.Sprintf("Replaced 1 occurrence of %s", search)
875 s = fmt.Sprintf("Nothing matched %s", search)
885 // ReplaceAllCmd replaces search term all at once
886 func (h *BufPane) ReplaceAllCmd(args []string) {
887 // aliased to Replace command
888 h.ReplaceCmd(append(args, "-a"))
891 // TermCmd opens a terminal in the current view
892 func (h *BufPane) TermCmd(args []string) {
895 if !TermEmuSupported {
896 InfoBar.Error("Terminal emulator not supported on this system")
901 sh := os.Getenv("SHELL")
903 InfoBar.Error("Shell environment not found")
909 term := func(i int, newtab bool) {
910 t := new(shell.Terminal)
911 err := t.Start(args, false, true, nil, nil)
921 id = MainTab().Panes[0].ID()
923 MainTab().Panes[i].Close()
927 tp, err := NewTermPane(v.X, v.Y, v.Width, v.Height, t, id, MainTab())
932 MainTab().Panes[i] = tp
933 MainTab().SetActive(i)
936 // If there is only one open file we make a new tab instead of overwriting it
937 newtab := len(MainTab().Panes) == 1 && len(Tabs.List) == 1
944 for i, p := range ps {
945 if p.ID() == h.ID() {
946 if h.Buf.Modified() {
947 InfoBar.YNPrompt("Save changes to "+h.Buf.GetName()+" before closing? (y,n,esc)", func(yes, canceled bool) {
948 if !canceled && !yes {
950 } else if !canceled && yes {
962 // HandleCommand handles input from the user
963 func (h *BufPane) HandleCommand(input string) {
964 args, err := shellquote.Split(input)
966 InfoBar.Error("Error parsing args ", err)
976 if _, ok := commands[inputCmd]; !ok {
977 InfoBar.Error("Unknown command ", inputCmd)
979 WriteLog("> " + input + "\n")
980 commands[inputCmd].action(h, args[1:])