15 luar "layeh.com/gopher-luar"
17 shellquote "github.com/kballard/go-shellquote"
18 lua "github.com/yuin/gopher-lua"
19 "github.com/zyedidia/micro/internal/buffer"
20 "github.com/zyedidia/micro/internal/config"
21 ulua "github.com/zyedidia/micro/internal/lua"
22 "github.com/zyedidia/micro/internal/screen"
23 "github.com/zyedidia/micro/internal/shell"
24 "github.com/zyedidia/micro/internal/util"
27 // A Command contains information about how to execute a command
28 // It has the action for that command as well as a completer function
30 action func(*BufPane, []string)
31 completer buffer.Completer
34 var commands map[string]Command
37 commands = map[string]Command{
38 "set": {(*BufPane).SetCmd, OptionValueComplete},
39 "reset": {(*BufPane).ResetCmd, OptionValueComplete},
40 "setlocal": {(*BufPane).SetLocalCmd, OptionValueComplete},
41 "show": {(*BufPane).ShowCmd, OptionComplete},
42 "showkey": {(*BufPane).ShowKeyCmd, nil},
43 "run": {(*BufPane).RunCmd, nil},
44 "bind": {(*BufPane).BindCmd, nil},
45 "unbind": {(*BufPane).UnbindCmd, nil},
46 "quit": {(*BufPane).QuitCmd, nil},
47 "goto": {(*BufPane).GotoCmd, nil},
48 "save": {(*BufPane).SaveCmd, nil},
49 "replace": {(*BufPane).ReplaceCmd, nil},
50 "replaceall": {(*BufPane).ReplaceAllCmd, nil},
51 "vsplit": {(*BufPane).VSplitCmd, buffer.FileComplete},
52 "hsplit": {(*BufPane).HSplitCmd, buffer.FileComplete},
53 "tab": {(*BufPane).NewTabCmd, buffer.FileComplete},
54 "help": {(*BufPane).HelpCmd, HelpComplete},
55 "eval": {(*BufPane).EvalCmd, nil},
56 "log": {(*BufPane).ToggleLogCmd, nil},
57 "plugin": {(*BufPane).PluginCmd, PluginComplete},
58 "reload": {(*BufPane).ReloadCmd, nil},
59 "reopen": {(*BufPane).ReopenCmd, nil},
60 "cd": {(*BufPane).CdCmd, buffer.FileComplete},
61 "pwd": {(*BufPane).PwdCmd, nil},
62 "open": {(*BufPane).OpenCmd, buffer.FileComplete},
63 "tabswitch": {(*BufPane).TabSwitchCmd, nil},
64 "term": {(*BufPane).TermCmd, nil},
65 "memusage": {(*BufPane).MemUsageCmd, nil},
66 "retab": {(*BufPane).RetabCmd, nil},
67 "raw": {(*BufPane).RawCmd, nil},
68 "textfilter": {(*BufPane).TextFilterCmd, nil},
72 // MakeCommand is a function to easily create new commands
73 // This can be called by plugins in Lua so that plugins can define their own commands
74 func LuaMakeCommand(name, function string, completer buffer.Completer) {
75 action := LuaFunctionCommand(function)
76 commands[name] = Command{action, completer}
79 // LuaFunctionCommand returns a normal function
80 // so that a command can be bound to a lua function
81 func LuaFunctionCommand(fn string) func(*BufPane, []string) {
82 luaFn := strings.Split(fn, ".")
86 plName, plFn := luaFn[0], luaFn[1]
87 pl := config.FindPlugin(plName)
91 return func(bp *BufPane, args []string) {
92 luaArgs := []lua.LValue{luar.New(ulua.L, bp), luar.New(ulua.L, args)}
93 _, err := pl.Call(plFn, luaArgs...)
95 screen.TermMessage(err)
100 // CommandEditAction returns a bindable function that opens a prompt with
101 // the given string and executes the command when the user presses
103 func CommandEditAction(prompt string) BufKeyAction {
104 return func(h *BufPane) bool {
105 InfoBar.Prompt("> ", prompt, "Command", nil, func(resp string, canceled bool) {
107 MainTab().CurPane().HandleCommand(resp)
114 // CommandAction returns a bindable function which executes the
116 func CommandAction(cmd string) BufKeyAction {
117 return func(h *BufPane) bool {
118 MainTab().CurPane().HandleCommand(cmd)
123 var PluginCmds = []string{"install", "remove", "update", "available", "list", "search"}
125 // PluginCmd installs, removes, updates, lists, or searches for given plugins
126 func (h *BufPane) PluginCmd(args []string) {
128 InfoBar.Error("Not enough arguments")
132 if h.Buf.Type != buffer.BTLog {
136 config.PluginCommand(buffer.LogBuf, args[0], args[1:])
139 // RetabCmd changes all spaces to tabs or all tabs to spaces
140 // depending on the user's settings
141 func (h *BufPane) RetabCmd(args []string) {
145 // RawCmd opens a new raw view which displays the escape sequences micro
146 // is receiving in real-time
147 func (h *BufPane) RawCmd(args []string) {
148 width, height := screen.Screen.Size()
149 iOffset := config.GetInfoBarOffset()
150 tp := NewTabFromPane(0, 0, width, height-iOffset, NewRawPane(nil))
152 Tabs.SetActive(len(Tabs.List) - 1)
155 // TextFilterCmd filters the selection through the command.
156 // Selection goes to the command input.
157 // On successful run command output replaces the current selection.
158 func (h *BufPane) TextFilterCmd(args []string) {
160 InfoBar.Error("usage: textfilter arguments")
163 sel := h.Cursor.GetSelection()
165 h.Cursor.SelectWord()
166 sel = h.Cursor.GetSelection()
168 var bout, berr bytes.Buffer
169 cmd := exec.Command(args[0], args[1:]...)
170 cmd.Stdin = strings.NewReader(string(sel))
175 InfoBar.Error(err.Error() + " " + berr.String())
178 h.Cursor.DeleteSelection()
179 h.Buf.Insert(h.Cursor.Loc, bout.String())
182 // TabSwitchCmd switches to a given tab either by name or by number
183 func (h *BufPane) TabSwitchCmd(args []string) {
185 num, err := strconv.Atoi(args[0])
187 // Check for tab with this name
190 for i, t := range Tabs.List {
191 if t.Panes[t.active].Name() == args[0] {
197 InfoBar.Error("Could not find tab: ", err)
201 if num >= 0 && num < len(Tabs.List) {
204 InfoBar.Error("Invalid tab index")
210 // CdCmd changes the current working directory
211 func (h *BufPane) CdCmd(args []string) {
213 path, err := util.ReplaceHome(args[0])
224 for _, b := range buffer.OpenBuffers {
226 b.Path, _ = util.MakeRelative(b.AbsPath, wd)
227 if p, _ := filepath.Abs(b.Path); !strings.Contains(p, wd) {
235 // MemUsageCmd prints micro's memory usage
236 // Alloc shows how many bytes are currently in use
237 // Sys shows how many bytes have been requested from the operating system
238 // NumGC shows how many times the GC has been run
239 // Note that Go commonly reserves more memory from the OS than is currently in-use/required
240 // Additionally, even if Go returns memory to the OS, the OS does not always claim it because
241 // there may be plenty of memory to spare
242 func (h *BufPane) MemUsageCmd(args []string) {
243 InfoBar.Message(util.GetMemStats())
246 // PwdCmd prints the current working directory
247 func (h *BufPane) PwdCmd(args []string) {
248 wd, err := os.Getwd()
250 InfoBar.Message(err.Error())
256 // OpenCmd opens a new buffer with a given filename
257 func (h *BufPane) OpenCmd(args []string) {
260 // the filename might or might not be quoted, so unquote first then join the strings.
261 args, err := shellquote.Split(filename)
263 InfoBar.Error("Error parsing args ", err)
269 filename = strings.Join(args, " ")
272 b, err := buffer.NewBufferFromFile(filename, buffer.BTDefault)
279 if h.Buf.Modified() {
280 InfoBar.YNPrompt("Save changes to "+h.Buf.GetName()+" before closing? (y,n,esc)", func(yes, canceled bool) {
281 if !canceled && !yes {
283 } else if !canceled && yes {
292 InfoBar.Error("No filename")
296 // ToggleLogCmd toggles the log view
297 func (h *BufPane) ToggleLogCmd(args []string) {
298 if h.Buf.Type != buffer.BTLog {
305 // ReloadCmd reloads all files (syntax files, colorschemes...)
306 func (h *BufPane) ReloadCmd(args []string) {
310 func ReloadConfig() {
311 config.InitRuntimeFiles()
312 err := config.ReadSettings()
314 screen.TermMessage(err)
316 config.InitGlobalSettings()
320 err = config.InitColorscheme()
322 screen.TermMessage(err)
325 for _, b := range buffer.OpenBuffers {
330 // ReopenCmd reopens the buffer (reload from disk)
331 func (h *BufPane) ReopenCmd(args []string) {
332 if h.Buf.Modified() {
333 InfoBar.YNPrompt("Save file before reopen?", func(yes, canceled bool) {
334 if !canceled && yes {
337 } else if !canceled {
346 func (h *BufPane) openHelp(page string) error {
347 if data, err := config.FindRuntimeFile(config.RTHelp, page).Data(); err != nil {
348 return errors.New(fmt.Sprint("Unable to load help text", page, "\n", err))
350 helpBuffer := buffer.NewBufferFromString(string(data), page+".md", buffer.BTHelp)
351 helpBuffer.SetName("Help " + page)
353 if h.Buf.Type == buffer.BTHelp {
354 h.OpenBuffer(helpBuffer)
356 h.HSplitBuf(helpBuffer)
362 // HelpCmd tries to open the given help page in a horizontal split
363 func (h *BufPane) HelpCmd(args []string) {
365 // Open the default help if the user just typed "> help"
368 if config.FindRuntimeFile(config.RTHelp, args[0]) != nil {
369 err := h.openHelp(args[0])
374 InfoBar.Error("Sorry, no help for ", args[0])
379 // VSplitCmd opens a vertical split with file given in the first argument
380 // If no file is given, it opens an empty buffer in a new split
381 func (h *BufPane) VSplitCmd(args []string) {
383 // Open an empty vertical split
388 buf, err := buffer.NewBufferFromFile(args[0], buffer.BTDefault)
397 // HSplitCmd opens a horizontal split with file given in the first argument
398 // If no file is given, it opens an empty buffer in a new split
399 func (h *BufPane) HSplitCmd(args []string) {
401 // Open an empty horizontal split
406 buf, err := buffer.NewBufferFromFile(args[0], buffer.BTDefault)
415 // EvalCmd evaluates a lua expression
416 func (h *BufPane) EvalCmd(args []string) {
417 InfoBar.Error("Eval unsupported")
420 // NewTabCmd opens the given file in a new tab
421 func (h *BufPane) NewTabCmd(args []string) {
422 width, height := screen.Screen.Size()
423 iOffset := config.GetInfoBarOffset()
425 for _, a := range args {
426 b, err := buffer.NewBufferFromFile(a, buffer.BTDefault)
431 tp := NewTabFromBuffer(0, 0, width, height-1-iOffset, b)
433 Tabs.SetActive(len(Tabs.List) - 1)
436 b := buffer.NewBufferFromString("", "", buffer.BTDefault)
437 tp := NewTabFromBuffer(0, 0, width, height-iOffset, b)
439 Tabs.SetActive(len(Tabs.List) - 1)
443 func SetGlobalOptionNative(option string, nativeValue interface{}) error {
445 for _, s := range config.LocalSettings {
453 config.GlobalSettings[option] = nativeValue
455 if option == "colorscheme" {
457 config.InitColorscheme()
458 for _, b := range buffer.OpenBuffers {
461 } else if option == "infobar" || option == "keymenu" {
463 } else if option == "mouse" {
464 if !nativeValue.(bool) {
465 screen.Screen.DisableMouse()
467 screen.Screen.EnableMouse()
469 // autosave option has been removed
470 // } else if option == "autosave" {
471 // if nativeValue.(float64) > 0 {
472 // config.SetAutoTime(int(nativeValue.(float64)))
473 // config.StartAutoSave()
475 // config.SetAutoTime(0)
477 } else if option == "paste" {
478 screen.Screen.SetPaste(nativeValue.(bool))
480 for _, pl := range config.Plugins {
481 if option == pl.Name {
482 if nativeValue.(bool) && !pl.Loaded {
484 _, err := pl.Call("init")
485 if err != nil && err != config.ErrNoSuchFunction {
486 screen.TermMessage(err)
488 } else if !nativeValue.(bool) && pl.Loaded {
489 _, err := pl.Call("deinit")
490 if err != nil && err != config.ErrNoSuchFunction {
491 screen.TermMessage(err)
499 for _, b := range buffer.OpenBuffers {
500 b.SetOptionNative(option, nativeValue)
503 return config.WriteSettings(config.ConfigDir + "/settings.json")
506 func SetGlobalOption(option, value string) error {
507 if _, ok := config.GlobalSettings[option]; !ok {
508 return config.ErrInvalidOption
511 nativeValue, err := config.GetNativeValue(option, config.GlobalSettings[option], value)
516 return SetGlobalOptionNative(option, nativeValue)
519 // ResetCmd resets a setting to its default value
520 func (h *BufPane) ResetCmd(args []string) {
522 InfoBar.Error("Not enough arguments")
528 defaultGlobals := config.DefaultGlobalSettings()
529 defaultLocals := config.DefaultCommonSettings()
531 if _, ok := defaultGlobals[option]; ok {
532 SetGlobalOptionNative(option, defaultGlobals[option])
535 if _, ok := defaultLocals[option]; ok {
536 h.Buf.SetOptionNative(option, defaultLocals[option])
539 InfoBar.Error(config.ErrInvalidOption)
542 // SetCmd sets an option
543 func (h *BufPane) SetCmd(args []string) {
545 InfoBar.Error("Not enough arguments")
552 err := SetGlobalOption(option, value)
553 if err == config.ErrInvalidOption {
554 err := h.Buf.SetOption(option, value)
558 } else if err != nil {
563 // SetLocalCmd sets an option local to the buffer
564 func (h *BufPane) SetLocalCmd(args []string) {
566 InfoBar.Error("Not enough arguments")
573 err := h.Buf.SetOption(option, value)
579 // ShowCmd shows the value of the given option
580 func (h *BufPane) ShowCmd(args []string) {
582 InfoBar.Error("Please provide an option to show")
586 var option interface{}
587 if opt, ok := h.Buf.Settings[args[0]]; ok {
589 } else if opt, ok := config.GlobalSettings[args[0]]; ok {
594 InfoBar.Error(args[0], " is not a valid option")
598 InfoBar.Message(option)
601 // ShowKeyCmd displays the action that a key is bound to
602 func (h *BufPane) ShowKeyCmd(args []string) {
604 InfoBar.Error("Please provide a key to show")
608 if action, ok := config.Bindings[args[0]]; ok {
609 InfoBar.Message(action)
611 InfoBar.Message(args[0], " has no binding")
615 // BindCmd creates a new keybinding
616 func (h *BufPane) BindCmd(args []string) {
618 InfoBar.Error("Not enough arguments")
622 _, err := TryBindKey(args[0], args[1], true)
628 // UnbindCmd binds a key to its default action
629 func (h *BufPane) UnbindCmd(args []string) {
631 InfoBar.Error("Not enough arguments")
635 err := UnbindKey(args[0])
641 // RunCmd runs a shell command in the background
642 func (h *BufPane) RunCmd(args []string) {
643 runf, err := shell.RunBackgroundShell(shellquote.Join(args...))
648 InfoBar.Message(runf())
654 // QuitCmd closes the main view
655 func (h *BufPane) QuitCmd(args []string) {
659 // GotoCmd is a command that will send the cursor to a certain
660 // position in the buffer
661 // For example: `goto line`, or `goto line:col`
662 func (h *BufPane) GotoCmd(args []string) {
664 InfoBar.Error("Not enough arguments")
666 h.RemoveAllMultiCursors()
667 if strings.Contains(args[0], ":") {
668 parts := strings.SplitN(args[0], ":", 2)
669 line, err := strconv.Atoi(parts[0])
674 col, err := strconv.Atoi(parts[1])
679 line = util.Clamp(line-1, 0, h.Buf.LinesNum()-1)
680 col = util.Clamp(col-1, 0, utf8.RuneCount(h.Buf.LineBytes(line)))
681 h.Cursor.GotoLoc(buffer.Loc{col, line})
683 line, err := strconv.Atoi(args[0])
688 line = util.Clamp(line-1, 0, h.Buf.LinesNum()-1)
689 h.Cursor.GotoLoc(buffer.Loc{0, line})
695 // SaveCmd saves the buffer optionally with an argument file name
696 func (h *BufPane) SaveCmd(args []string) {
700 h.Buf.SaveAs(args[0])
704 // ReplaceCmd runs search and replace
705 func (h *BufPane) ReplaceCmd(args []string) {
706 if len(args) < 2 || len(args) > 4 {
707 // We need to find both a search and replace expression
708 InfoBar.Error("Invalid replace statement: " + strings.Join(args, " "))
716 foundReplace := false
718 var replaceStr string
719 for _, arg := range args {
729 } else if !foundReplace {
733 InfoBar.Error("Invalid flag: " + arg)
740 search = regexp.QuoteMeta(search)
743 replace := []byte(replaceStr)
745 var regex *regexp.Regexp
747 if h.Buf.Settings["ignorecase"].(bool) {
748 regex, err = regexp.Compile("(?im)" + search)
750 regex, err = regexp.Compile("(?m)" + search)
753 // There was an error with the user's regex
759 start := h.Buf.Start()
760 // end := h.Buf.End()
761 // if h.Cursor.HasSelection() {
762 // start = h.Cursor.CurSelection[0]
763 // end = h.Cursor.CurSelection[1]
766 nreplaced = h.Buf.ReplaceRegex(start, h.Buf.End(), regex, replace)
768 inRange := func(l buffer.Loc) bool {
769 return l.GreaterEqual(start) && l.LessEqual(h.Buf.End())
774 var doReplacement func()
775 doReplacement = func() {
776 locs, found, err := h.Buf.FindNext(search, start, h.Buf.End(), searchLoc, true, !noRegex)
781 if !found || !inRange(locs[0]) || !inRange(locs[1]) {
782 h.Cursor.ResetSelection()
783 h.Buf.RelocateCursors()
787 h.Cursor.SetSelectionStart(locs[0])
788 h.Cursor.SetSelectionEnd(locs[1])
790 InfoBar.YNPrompt("Perform replacement (y,n,esc)", func(yes, canceled bool) {
791 if !canceled && yes {
792 h.Buf.Replace(locs[0], locs[1], replaceStr)
795 searchLoc.X += utf8.RuneCount(replace)
796 h.Cursor.Loc = searchLoc
798 } else if !canceled && !yes {
800 searchLoc.X += utf8.RuneCount(replace)
802 h.Cursor.ResetSelection()
803 h.Buf.RelocateCursors()
814 h.Buf.RelocateCursors()
817 InfoBar.Message("Replaced ", nreplaced, " occurrences of ", search)
818 } else if nreplaced == 1 {
819 InfoBar.Message("Replaced ", nreplaced, " occurrence of ", search)
821 InfoBar.Message("Nothing matched ", search)
825 // ReplaceAllCmd replaces search term all at once
826 func (h *BufPane) ReplaceAllCmd(args []string) {
827 // aliased to Replace command
828 h.ReplaceCmd(append(args, "-a"))
831 // TermCmd opens a terminal in the current view
832 func (h *BufPane) TermCmd(args []string) {
836 sh := os.Getenv("SHELL")
838 InfoBar.Error("Shell environment not found")
844 term := func(i int, newtab bool) {
845 t := new(shell.Terminal)
846 t.Start(args, false, true, "", nil)
852 id = MainTab().Panes[0].ID()
854 MainTab().Panes[i].Close()
858 MainTab().Panes[i] = NewTermPane(v.X, v.Y, v.Width, v.Height, t, id, MainTab())
859 MainTab().SetActive(i)
862 // If there is only one open file we make a new tab instead of overwriting it
863 newtab := len(MainTab().Panes) == 1 && len(Tabs.List) == 1
870 for i, p := range ps {
871 if p.ID() == h.ID() {
872 if h.Buf.Modified() {
873 InfoBar.YNPrompt("Save changes to "+h.Buf.GetName()+" before closing? (y,n,esc)", func(yes, canceled bool) {
874 if !canceled && !yes {
876 } else if !canceled && yes {
888 // HandleCommand handles input from the user
889 func (h *BufPane) HandleCommand(input string) {
890 args, err := shellquote.Split(input)
892 InfoBar.Error("Error parsing args ", err)
902 if _, ok := commands[inputCmd]; !ok {
903 InfoBar.Error("Unknown command ", inputCmd)
905 WriteLog("> " + input + "\n")
906 commands[inputCmd].action(h, args[1:])