package action import ( "bytes" "errors" "fmt" "os" "path/filepath" "regexp" "strconv" "strings" "unicode/utf8" luar "layeh.com/gopher-luar" lua "github.com/yuin/gopher-lua" "github.com/zyedidia/micro/internal/buffer" "github.com/zyedidia/micro/internal/config" ulua "github.com/zyedidia/micro/internal/lua" "github.com/zyedidia/micro/internal/screen" "github.com/zyedidia/micro/internal/shell" "github.com/zyedidia/micro/internal/util" "github.com/zyedidia/micro/pkg/shellwords" ) // A Command contains information about how to execute a command // It has the action for that command as well as a completer function type Command struct { action func(*BufPane, []string) completer buffer.Completer } var commands map[string]Command func InitCommands() { commands = map[string]Command{ "set": Command{(*BufPane).SetCmd, OptionValueComplete}, "reset": Command{(*BufPane).ResetCmd, OptionValueComplete}, "setlocal": Command{(*BufPane).SetLocalCmd, OptionValueComplete}, "show": Command{(*BufPane).ShowCmd, OptionComplete}, "showkey": Command{(*BufPane).ShowKeyCmd, nil}, "run": Command{(*BufPane).RunCmd, nil}, "bind": Command{(*BufPane).BindCmd, nil}, "unbind": Command{(*BufPane).UnbindCmd, nil}, "quit": Command{(*BufPane).QuitCmd, nil}, "goto": Command{(*BufPane).GotoCmd, nil}, "save": Command{(*BufPane).SaveCmd, nil}, "replace": Command{(*BufPane).ReplaceCmd, nil}, "replaceall": Command{(*BufPane).ReplaceAllCmd, nil}, "vsplit": Command{(*BufPane).VSplitCmd, buffer.FileComplete}, "hsplit": Command{(*BufPane).HSplitCmd, buffer.FileComplete}, "tab": Command{(*BufPane).NewTabCmd, buffer.FileComplete}, "help": Command{(*BufPane).HelpCmd, HelpComplete}, "eval": Command{(*BufPane).EvalCmd, nil}, "log": Command{(*BufPane).ToggleLogCmd, nil}, "plugin": Command{(*BufPane).PluginCmd, PluginComplete}, "reload": Command{(*BufPane).ReloadCmd, nil}, "reopen": Command{(*BufPane).ReopenCmd, nil}, "cd": Command{(*BufPane).CdCmd, buffer.FileComplete}, "pwd": Command{(*BufPane).PwdCmd, nil}, "open": Command{(*BufPane).OpenCmd, buffer.FileComplete}, "tabswitch": Command{(*BufPane).TabSwitchCmd, nil}, "term": Command{(*BufPane).TermCmd, nil}, "memusage": Command{(*BufPane).MemUsageCmd, nil}, "retab": Command{(*BufPane).RetabCmd, nil}, "raw": Command{(*BufPane).RawCmd, nil}, } } // MakeCommand is a function to easily create new commands // This can be called by plugins in Lua so that plugins can define their own commands func LuaMakeCommand(name, function string, completer buffer.Completer) { action := LuaFunctionCommand(function) commands[name] = Command{action, completer} } // LuaFunctionCommand returns a normal function // so that a command can be bound to a lua function func LuaFunctionCommand(fn string) func(*BufPane, []string) { luaFn := strings.Split(fn, ".") if len(luaFn) <= 1 { return nil } plName, plFn := luaFn[0], luaFn[1] pl := config.FindPlugin(plName) if pl == nil { return nil } return func(bp *BufPane, args []string) { var luaArgs []lua.LValue luaArgs = append(luaArgs, luar.New(ulua.L, bp)) for _, v := range args { luaArgs = append(luaArgs, luar.New(ulua.L, v)) } _, err := pl.Call(plFn, luaArgs...) if err != nil { screen.TermMessage(err) } } } // CommandEditAction returns a bindable function that opens a prompt with // the given string and executes the command when the user presses // enter func CommandEditAction(prompt string) BufKeyAction { return func(h *BufPane) bool { InfoBar.Prompt("> ", prompt, "Command", nil, func(resp string, canceled bool) { if !canceled { MainTab().CurPane().HandleCommand(resp) } }) return false } } // CommandAction returns a bindable function which executes the // given command func CommandAction(cmd string) BufKeyAction { return func(h *BufPane) bool { MainTab().CurPane().HandleCommand(cmd) return false } } var PluginCmds = []string{"list", "info", "version"} // PluginCmd installs, removes, updates, lists, or searches for given plugins func (h *BufPane) PluginCmd(args []string) { if len(args) <= 0 { InfoBar.Error("Not enough arguments, see 'help commands'") return } valid := true switch args[0] { case "list": for _, pl := range config.Plugins { var en string if pl.IsEnabled() { en = "enabled" } else { en = "disabled" } WriteLog(fmt.Sprintf("%s: %s", pl.Name, en)) if pl.Default { WriteLog(" (default)\n") } else { WriteLog("\n") } } WriteLog("Default plugins come pre-installed with micro.") case "version": if len(args) <= 1 { InfoBar.Error("No plugin provided to give info for") return } found := false for _, pl := range config.Plugins { if pl.Name == args[1] { found = true if pl.Info == nil { InfoBar.Message("Sorry no version for", pl.Name) return } WriteLog("Version: " + pl.Info.Vstr + "\n") } } if !found { InfoBar.Message(args[1], "is not installed") } case "info": if len(args) <= 1 { InfoBar.Error("No plugin provided to give info for") return } found := false for _, pl := range config.Plugins { if pl.Name == args[1] { found = true if pl.Info == nil { InfoBar.Message("Sorry no info for ", pl.Name) return } var buffer bytes.Buffer buffer.WriteString("Name: ") buffer.WriteString(pl.Info.Name) buffer.WriteString("\n") buffer.WriteString("Description: ") buffer.WriteString(pl.Info.Desc) buffer.WriteString("\n") buffer.WriteString("Website: ") buffer.WriteString(pl.Info.Site) buffer.WriteString("\n") buffer.WriteString("Installation link: ") buffer.WriteString(pl.Info.Install) buffer.WriteString("\n") buffer.WriteString("Version: ") buffer.WriteString(pl.Info.Vstr) buffer.WriteString("\n") buffer.WriteString("Requirements:") buffer.WriteString("\n") for _, r := range pl.Info.Require { buffer.WriteString(" - ") buffer.WriteString(r) buffer.WriteString("\n") } WriteLog(buffer.String()) } } if !found { InfoBar.Message(args[1], "is not installed") return } default: InfoBar.Error("Not a valid plugin command") return } if valid && h.Buf.Type != buffer.BTLog { OpenLogBuf(h) } } // RetabCmd changes all spaces to tabs or all tabs to spaces // depending on the user's settings func (h *BufPane) RetabCmd(args []string) { h.Buf.Retab() } // RawCmd opens a new raw view which displays the escape sequences micro // is receiving in real-time func (h *BufPane) RawCmd(args []string) { width, height := screen.Screen.Size() iOffset := config.GetInfoBarOffset() tp := NewTabFromPane(0, 0, width, height-iOffset, NewRawPane()) Tabs.AddTab(tp) Tabs.SetActive(len(Tabs.List) - 1) } // TabSwitchCmd switches to a given tab either by name or by number func (h *BufPane) TabSwitchCmd(args []string) { if len(args) > 0 { num, err := strconv.Atoi(args[0]) if err != nil { // Check for tab with this name found := false for i, t := range Tabs.List { if t.Panes[t.active].Name() == args[0] { Tabs.SetActive(i) found = true } } if !found { InfoBar.Error("Could not find tab: ", err) } } else { num-- if num >= 0 && num < len(Tabs.List) { Tabs.SetActive(num) } else { InfoBar.Error("Invalid tab index") } } } } // CdCmd changes the current working directory func (h *BufPane) CdCmd(args []string) { if len(args) > 0 { path, err := util.ReplaceHome(args[0]) if err != nil { InfoBar.Error(err) return } err = os.Chdir(path) if err != nil { InfoBar.Error(err) return } wd, _ := os.Getwd() for _, b := range buffer.OpenBuffers { if len(b.Path) > 0 { b.Path, _ = util.MakeRelative(b.AbsPath, wd) if p, _ := filepath.Abs(b.Path); !strings.Contains(p, wd) { b.Path = b.AbsPath } } } } } // MemUsageCmd prints micro's memory usage // Alloc shows how many bytes are currently in use // Sys shows how many bytes have been requested from the operating system // NumGC shows how many times the GC has been run // Note that Go commonly reserves more memory from the OS than is currently in-use/required // Additionally, even if Go returns memory to the OS, the OS does not always claim it because // there may be plenty of memory to spare func (h *BufPane) MemUsageCmd(args []string) { InfoBar.Message(util.GetMemStats()) } // PwdCmd prints the current working directory func (h *BufPane) PwdCmd(args []string) { wd, err := os.Getwd() if err != nil { InfoBar.Message(err.Error()) } else { InfoBar.Message(wd) } } // OpenCmd opens a new buffer with a given filename func (h *BufPane) OpenCmd(args []string) { if len(args) > 0 { filename := args[0] // the filename might or might not be quoted, so unquote first then join the strings. args, err := shellwords.Split(filename) if err != nil { InfoBar.Error("Error parsing args ", err) return } filename = strings.Join(args, " ") open := func() { b, err := buffer.NewBufferFromFile(filename, buffer.BTDefault) if err != nil { InfoBar.Error(err) return } h.OpenBuffer(b) } if h.Buf.Modified() { InfoBar.YNPrompt("Save changes to "+h.Buf.GetName()+" before closing? (y,n,esc)", func(yes, canceled bool) { if !canceled && !yes { open() } else if !canceled && yes { h.Save() open() } }) } else { open() } } else { InfoBar.Error("No filename") } } // ToggleLogCmd toggles the log view func (h *BufPane) ToggleLogCmd(args []string) { if h.Buf.Type != buffer.BTLog { OpenLogBuf(h) } else { h.Quit() } } // ReloadCmd reloads all files (syntax files, colorschemes...) func (h *BufPane) ReloadCmd(args []string) { ReloadConfig() } func ReloadConfig() { config.InitRuntimeFiles() err := config.ReadSettings() if err != nil { screen.TermMessage(err) } config.InitGlobalSettings() InitBindings() InitCommands() err = config.InitColorscheme() if err != nil { screen.TermMessage(err) } for _, b := range buffer.OpenBuffers { b.UpdateRules() } } // ReopenCmd reopens the buffer (reload from disk) func (h *BufPane) ReopenCmd(args []string) { if h.Buf.Modified() { InfoBar.YNPrompt("Save file before reopen?", func(yes, canceled bool) { if !canceled && yes { h.Save() h.Buf.ReOpen() } else if !canceled { h.Buf.ReOpen() } }) } else { h.Buf.ReOpen() } } func (h *BufPane) openHelp(page string) error { if data, err := config.FindRuntimeFile(config.RTHelp, page).Data(); err != nil { return errors.New(fmt.Sprint("Unable to load help text", page, "\n", err)) } else { helpBuffer := buffer.NewBufferFromString(string(data), page+".md", buffer.BTHelp) helpBuffer.SetName("Help " + page) if h.Buf.Type == buffer.BTHelp { h.OpenBuffer(helpBuffer) } else { h.HSplitBuf(helpBuffer) } } return nil } // HelpCmd tries to open the given help page in a horizontal split func (h *BufPane) HelpCmd(args []string) { if len(args) < 1 { // Open the default help if the user just typed "> help" h.openHelp("help") } else { if config.FindRuntimeFile(config.RTHelp, args[0]) != nil { err := h.openHelp(args[0]) if err != nil { InfoBar.Error(err) } } else { InfoBar.Error("Sorry, no help for ", args[0]) } } } // VSplitCmd opens a vertical split with file given in the first argument // If no file is given, it opens an empty buffer in a new split func (h *BufPane) VSplitCmd(args []string) { if len(args) == 0 { // Open an empty vertical split h.VSplitAction() return } buf, err := buffer.NewBufferFromFile(args[0], buffer.BTDefault) if err != nil { InfoBar.Error(err) return } h.VSplitBuf(buf) } // HSplitCmd opens a horizontal split with file given in the first argument // If no file is given, it opens an empty buffer in a new split func (h *BufPane) HSplitCmd(args []string) { if len(args) == 0 { // Open an empty horizontal split h.HSplitAction() return } buf, err := buffer.NewBufferFromFile(args[0], buffer.BTDefault) if err != nil { InfoBar.Error(err) return } h.HSplitBuf(buf) } // EvalCmd evaluates a lua expression func (h *BufPane) EvalCmd(args []string) { } // NewTabCmd opens the given file in a new tab func (h *BufPane) NewTabCmd(args []string) { width, height := screen.Screen.Size() iOffset := config.GetInfoBarOffset() if len(args) > 0 { for _, a := range args { b, err := buffer.NewBufferFromFile(a, buffer.BTDefault) if err != nil { InfoBar.Error(err) return } tp := NewTabFromBuffer(0, 0, width, height-1-iOffset, b) Tabs.AddTab(tp) Tabs.SetActive(len(Tabs.List) - 1) } } else { b := buffer.NewBufferFromString("", "", buffer.BTDefault) tp := NewTabFromBuffer(0, 0, width, height-iOffset, b) Tabs.AddTab(tp) Tabs.SetActive(len(Tabs.List) - 1) } } func SetGlobalOptionNative(option string, nativeValue interface{}) error { config.GlobalSettings[option] = nativeValue if option == "colorscheme" { // LoadSyntaxFiles() config.InitColorscheme() for _, b := range buffer.OpenBuffers { b.UpdateRules() } } else if option == "infobar" || option == "keymenu" { Tabs.Resize() } else if option == "mouse" { if !nativeValue.(bool) { screen.Screen.DisableMouse() } else { screen.Screen.EnableMouse() } } else if option == "autosave" { if nativeValue.(float64) > 0 { config.SetAutoTime(int(nativeValue.(float64))) config.StartAutoSave() } else { config.SetAutoTime(0) } } else { for _, pl := range config.Plugins { if option == pl.Name { if nativeValue.(bool) && !pl.Loaded { pl.Load() pl.Call("init") } else if !nativeValue.(bool) && pl.Loaded { pl.Call("deinit") } } } } for _, b := range buffer.OpenBuffers { b.SetOptionNative(option, nativeValue) } return config.WriteSettings(config.ConfigDir + "/settings.json") } func SetGlobalOption(option, value string) error { if _, ok := config.GlobalSettings[option]; !ok { return config.ErrInvalidOption } nativeValue, err := config.GetNativeValue(option, config.GlobalSettings[option], value) if err != nil { return err } return SetGlobalOptionNative(option, nativeValue) } // ResetCmd resets a setting to its default value func (h *BufPane) ResetCmd(args []string) { if len(args) < 1 { InfoBar.Error("Not enough arguments") return } option := args[0] defaultGlobals := config.DefaultGlobalSettings() defaultLocals := config.DefaultCommonSettings() if _, ok := defaultGlobals[option]; ok { SetGlobalOptionNative(option, defaultGlobals[option]) return } if _, ok := defaultLocals[option]; ok { h.Buf.SetOptionNative(option, defaultLocals[option]) return } InfoBar.Error(config.ErrInvalidOption) } // SetCmd sets an option func (h *BufPane) SetCmd(args []string) { if len(args) < 2 { InfoBar.Error("Not enough arguments") return } option := args[0] value := args[1] err := SetGlobalOption(option, value) if err == config.ErrInvalidOption { err := h.Buf.SetOption(option, value) if err != nil { InfoBar.Error(err) } } else if err != nil { InfoBar.Error(err) } } // SetLocalCmd sets an option local to the buffer func (h *BufPane) SetLocalCmd(args []string) { if len(args) < 2 { InfoBar.Error("Not enough arguments") return } option := args[0] value := args[1] err := h.Buf.SetOption(option, value) if err != nil { InfoBar.Error(err) } } // ShowCmd shows the value of the given option func (h *BufPane) ShowCmd(args []string) { if len(args) < 1 { InfoBar.Error("Please provide an option to show") return } var option interface{} if opt, ok := h.Buf.Settings[args[0]]; ok { option = opt } else if opt, ok := config.GlobalSettings[args[0]]; ok { option = opt } if option == nil { InfoBar.Error(args[0], " is not a valid option") return } InfoBar.Message(option) } // ShowKeyCmd displays the action that a key is bound to func (h *BufPane) ShowKeyCmd(args []string) { if len(args) < 1 { InfoBar.Error("Please provide a key to show") return } if action, ok := config.Bindings[args[0]]; ok { InfoBar.Message(action) } else { InfoBar.Message(args[0], " has no binding") } } // BindCmd creates a new keybinding func (h *BufPane) BindCmd(args []string) { if len(args) < 2 { InfoBar.Error("Not enough arguments") return } _, err := TryBindKey(args[0], args[1], true) if err != nil { InfoBar.Error(err) } } // UnbindCmd binds a key to its default action func (h *BufPane) UnbindCmd(args []string) { if len(args) < 1 { InfoBar.Error("Not enough arguements") return } err := UnbindKey(args[0]) if err != nil { InfoBar.Error(err) } } // RunCmd runs a shell command in the background func (h *BufPane) RunCmd(args []string) { runf, err := shell.RunBackgroundShell(shellwords.Join(args...)) if err != nil { InfoBar.Error(err) } else { go func() { InfoBar.Message(runf()) screen.Redraw() }() } } // QuitCmd closes the main view func (h *BufPane) QuitCmd(args []string) { h.Quit() } // GotoCmd is a command that will send the cursor to a certain // position in the buffer // For example: `goto line`, or `goto line:col` func (h *BufPane) GotoCmd(args []string) { if len(args) <= 0 { InfoBar.Error("Not enough arguments") } else { h.RemoveAllMultiCursors() if strings.Contains(args[0], ":") { parts := strings.SplitN(args[0], ":", 2) line, err := strconv.Atoi(parts[0]) if err != nil { InfoBar.Error(err) return } col, err := strconv.Atoi(parts[1]) if err != nil { InfoBar.Error(err) return } line = util.Clamp(line-1, 0, h.Buf.LinesNum()-1) col = util.Clamp(col-1, 0, utf8.RuneCount(h.Buf.LineBytes(line))) h.Cursor.GotoLoc(buffer.Loc{col, line}) } else { line, err := strconv.Atoi(args[0]) if err != nil { InfoBar.Error(err) return } line = util.Clamp(line-1, 0, h.Buf.LinesNum()-1) h.Cursor.GotoLoc(buffer.Loc{0, line}) } } } // SaveCmd saves the buffer optionally with an argument file name func (h *BufPane) SaveCmd(args []string) { if len(args) == 0 { h.Save() } else { h.Buf.SaveAs(args[0]) } } // ReplaceCmd runs search and replace func (h *BufPane) ReplaceCmd(args []string) { if len(args) < 2 || len(args) > 4 { // We need to find both a search and replace expression InfoBar.Error("Invalid replace statement: " + strings.Join(args, " ")) return } all := false noRegex := false foundSearch := false foundReplace := false var search string var replaceStr string for _, arg := range args { switch arg { case "-a": all = true case "-l": noRegex = true default: if !foundSearch { foundSearch = true search = arg } else if !foundReplace { foundReplace = true replaceStr = arg } else { InfoBar.Error("Invalid flag: " + arg) return } } } if noRegex { search = regexp.QuoteMeta(search) } replace := []byte(replaceStr) var regex *regexp.Regexp var err error if h.Buf.Settings["ignorecase"].(bool) { regex, err = regexp.Compile("(?im)" + search) } else { regex, err = regexp.Compile("(?m)" + search) } if err != nil { // There was an error with the user's regex InfoBar.Error(err) return } nreplaced := 0 start := h.Buf.Start() end := h.Buf.End() if h.Cursor.HasSelection() { start = h.Cursor.CurSelection[0] end = h.Cursor.CurSelection[1] } if all { nreplaced = h.Buf.ReplaceRegex(start, end, regex, replace) } else { inRange := func(l buffer.Loc) bool { return l.GreaterEqual(start) && l.LessThan(end) } searchLoc := start searching := true var doReplacement func() doReplacement = func() { locs, found, err := h.Buf.FindNext(search, start, end, searchLoc, true, !noRegex) if err != nil { InfoBar.Error(err) return } if !found || !inRange(locs[0]) || !inRange(locs[1]) { h.Cursor.ResetSelection() h.Buf.RelocateCursors() return } h.Cursor.SetSelectionStart(locs[0]) h.Cursor.SetSelectionEnd(locs[1]) InfoBar.YNPrompt("Perform replacement (y,n,esc)", func(yes, canceled bool) { if !canceled && yes { h.Buf.Replace(locs[0], locs[1], replaceStr) searchLoc = locs[0] searchLoc.X += utf8.RuneCount(replace) h.Cursor.Loc = searchLoc nreplaced++ } else if !canceled && !yes { searchLoc = locs[0] searchLoc.X += utf8.RuneCount(replace) } else if canceled { h.Cursor.ResetSelection() h.Buf.RelocateCursors() return } if searching { doReplacement() } }) } doReplacement() } h.Buf.RelocateCursors() if nreplaced > 1 { InfoBar.Message("Replaced ", nreplaced, " occurrences of ", search) } else if nreplaced == 1 { InfoBar.Message("Replaced ", nreplaced, " occurrence of ", search) } else { InfoBar.Message("Nothing matched ", search) } } // ReplaceAllCmd replaces search term all at once func (h *BufPane) ReplaceAllCmd(args []string) { // aliased to Replace command h.ReplaceCmd(append(args, "-a")) } // TermCmd opens a terminal in the current view func (h *BufPane) TermCmd(args []string) { ps := MainTab().Panes if len(args) == 0 { sh := os.Getenv("SHELL") if sh == "" { InfoBar.Error("Shell environment not found") return } args = []string{sh} } term := func(i int, newtab bool) { t := new(shell.Terminal) t.Start(args, false, true, "", nil) id := h.ID() if newtab { h.AddTab() i = 0 id = MainTab().Panes[0].ID() } else { MainTab().Panes[i].Close() } v := h.GetView() MainTab().Panes[i] = NewTermPane(v.X, v.Y, v.Width, v.Height, t, id) MainTab().SetActive(i) } // If there is only one open file we make a new tab instead of overwriting it newtab := len(MainTab().Panes) == 1 && len(Tabs.List) == 1 if newtab { term(0, true) return } for i, p := range ps { if p.ID() == h.ID() { if h.Buf.Modified() { InfoBar.YNPrompt("Save changes to "+h.Buf.GetName()+" before closing? (y,n,esc)", func(yes, canceled bool) { if !canceled && !yes { term(i, false) } else if !canceled && yes { h.Save() term(i, false) } }) } else { term(i, false) } } } } // HandleCommand handles input from the user func (h *BufPane) HandleCommand(input string) { args, err := shellwords.Split(input) if err != nil { InfoBar.Error("Error parsing args ", err) return } inputCmd := args[0] if _, ok := commands[inputCmd]; !ok { InfoBar.Error("Unknown command ", inputCmd) } else { WriteLog("> " + input + "\n") commands[inputCmd].action(h, args[1:]) WriteLog("\n") } }