X-Git-Url: https://git.lizzy.rs/?a=blobdiff_plain;f=cmd%2Fmicro%2Fcommand.go;h=b41a3db4748920cbaacf7902524817d3c6c585e8;hb=f9cb99b35fa36099ac254e129e2668ada6a5034f;hp=a6d5cc5e3fedafc6528fc9850bfc1b76b65e17f8;hpb=1c127a6c3fb68573da6d30cc8f69dcad43bf4ed5;p=micro.git diff --git a/cmd/micro/command.go b/cmd/micro/command.go index a6d5cc5e..b41a3db4 100644 --- a/cmd/micro/command.go +++ b/cmd/micro/command.go @@ -2,13 +2,304 @@ package main import ( "bytes" + "io/ioutil" "os" "os/exec" "os/signal" "regexp" "strings" + + "github.com/mitchellh/go-homedir" ) +type Command struct { + action func([]string) + completions []Completion +} + +type StrCommand struct { + action string + completions []Completion +} + +var commands map[string]Command + +var commandActions = map[string]func([]string){ + "Set": Set, + "Run": Run, + "Bind": Bind, + "Quit": Quit, + "Save": Save, + "Replace": Replace, + "VSplit": VSplit, + "HSplit": HSplit, + "Tab": NewTab, + "Help": Help, +} + +// InitCommands initializes the default commands +func InitCommands() { + commands = make(map[string]Command) + + defaults := DefaultCommands() + parseCommands(defaults) +} + +func parseCommands(userCommands map[string]StrCommand) { + for k, v := range userCommands { + MakeCommand(k, v.action, v.completions...) + } +} + +// 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 MakeCommand(name, function string, completions ...Completion) { + action := commandActions[function] + if _, ok := commandActions[function]; !ok { + // If the user seems to be binding a function that doesn't exist + // We hope that it's a lua function that exists and bind it to that + action = LuaFunctionCommand(function) + } + + commands[name] = Command{action, completions} +} + +// DefaultCommands returns a map containing micro's default commands +func DefaultCommands() map[string]StrCommand { + return map[string]StrCommand{ + "set": StrCommand{"Set", []Completion{OptionCompletion, NoCompletion}}, + "bind": StrCommand{"Bind", []Completion{NoCompletion}}, + "run": StrCommand{"Run", []Completion{NoCompletion}}, + "quit": StrCommand{"Quit", []Completion{NoCompletion}}, + "save": StrCommand{"Save", []Completion{NoCompletion}}, + "replace": StrCommand{"Replace", []Completion{NoCompletion}}, + "vsplit": StrCommand{"VSplit", []Completion{FileCompletion, NoCompletion}}, + "hsplit": StrCommand{"HSplit", []Completion{FileCompletion, NoCompletion}}, + "tab": StrCommand{"Tab", []Completion{FileCompletion, NoCompletion}}, + "help": StrCommand{"Help", []Completion{HelpCompletion, NoCompletion}}, + } +} + +// Help tries to open the given help page in a horizontal split +func Help(args []string) { + if len(args) < 1 { + // Open the default help if the user just typed "> help" + CurView().openHelp("help") + } else { + helpPage := args[0] + if _, ok := helpPages[helpPage]; ok { + CurView().openHelp(helpPage) + } else { + messenger.Error("Sorry, no help for ", helpPage) + } + } +} + +// VSplit 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 VSplit(args []string) { + if len(args) == 0 { + CurView().VSplit(NewBuffer([]byte{}, "")) + } else { + filename := args[0] + home, _ := homedir.Dir() + filename = strings.Replace(filename, "~", home, 1) + file, err := ioutil.ReadFile(filename) + + var buf *Buffer + if err != nil { + // File does not exist -- create an empty buffer with that name + buf = NewBuffer([]byte{}, filename) + } else { + buf = NewBuffer(file, filename) + } + CurView().VSplit(buf) + } +} + +// HSplit 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 HSplit(args []string) { + if len(args) == 0 { + CurView().HSplit(NewBuffer([]byte{}, "")) + } else { + filename := args[0] + home, _ := homedir.Dir() + filename = strings.Replace(filename, "~", home, 1) + file, err := ioutil.ReadFile(filename) + + var buf *Buffer + if err != nil { + // File does not exist -- create an empty buffer with that name + buf = NewBuffer([]byte{}, filename) + } else { + buf = NewBuffer(file, filename) + } + CurView().HSplit(buf) + } +} + +// NewTab opens the given file in a new tab +func NewTab(args []string) { + if len(args) == 0 { + CurView().AddTab(true) + } else { + filename := args[0] + home, _ := homedir.Dir() + filename = strings.Replace(filename, "~", home, 1) + file, _ := ioutil.ReadFile(filename) + + tab := NewTabFromView(NewView(NewBuffer(file, filename))) + tab.SetNum(len(tabs)) + tabs = append(tabs, tab) + curTab++ + if len(tabs) == 2 { + for _, t := range tabs { + for _, v := range t.views { + v.ToggleTabbar() + } + } + } + } +} + +// Set sets an option +func Set(args []string) { + if len(args) < 2 { + return + } + + option := strings.TrimSpace(args[0]) + value := strings.TrimSpace(args[1]) + + SetOptionAndSettings(option, value) +} + +// Bind creates a new keybinding +func Bind(args []string) { + if len(args) != 2 { + messenger.Error("Incorrect number of arguments") + return + } + BindKey(args[0], args[1]) +} + +// Run runs a shell command in the background +func Run(args []string) { + // Run a shell command in the background (openTerm is false) + HandleShellCommand(strings.Join(args, " "), false) +} + +// Quit closes the main view +func Quit(args []string) { + // Close the main view + CurView().Quit(true) +} + +// Save saves the buffer in the main view +func Save(args []string) { + // Save the main view + CurView().Save(true) +} + +// Replace runs search and replace +func Replace(args []string) { + // This is a regex to parse the replace expression + // We allow no quotes if there are no spaces, but if you want to search + // for or replace an expression with spaces, you can add double quotes + r := regexp.MustCompile(`"[^"\\]*(?:\\.[^"\\]*)*"|[^\s]*`) + replaceCmd := r.FindAllString(strings.Join(args, " "), -1) + if len(replaceCmd) < 2 { + // We need to find both a search and replace expression + messenger.Error("Invalid replace statement: " + strings.Join(args, " ")) + return + } + + var flags string + if len(replaceCmd) == 3 { + // The user included some flags + flags = replaceCmd[2] + } + + search := string(replaceCmd[0]) + replace := string(replaceCmd[1]) + + // If the search and replace expressions have quotes, we need to remove those + if strings.HasPrefix(search, `"`) && strings.HasSuffix(search, `"`) { + search = search[1 : len(search)-1] + } + if strings.HasPrefix(replace, `"`) && strings.HasSuffix(replace, `"`) { + replace = replace[1 : len(replace)-1] + } + + // We replace all escaped double quotes to real double quotes + search = strings.Replace(search, `\"`, `"`, -1) + replace = strings.Replace(replace, `\"`, `"`, -1) + // Replace some things so users can actually insert newlines and tabs in replacements + replace = strings.Replace(replace, "\\n", "\n", -1) + replace = strings.Replace(replace, "\\t", "\t", -1) + + regex, err := regexp.Compile(search) + if err != nil { + // There was an error with the user's regex + messenger.Error(err.Error()) + return + } + + view := CurView() + + found := 0 + for { + match := regex.FindStringIndex(view.Buf.String()) + if match == nil { + break + } + found++ + if strings.Contains(flags, "c") { + // The 'check' flag was used + Search(search, view, true) + view.Relocate() + if settings["syntax"].(bool) { + view.matches = Match(view) + } + RedrawAll() + choice, canceled := messenger.YesNoPrompt("Perform replacement? (y,n)") + if canceled { + if view.Cursor.HasSelection() { + view.Cursor.Loc = view.Cursor.CurSelection[0] + view.Cursor.ResetSelection() + } + messenger.Reset() + return + } + if choice { + view.Cursor.DeleteSelection() + view.Buf.Insert(FromCharPos(match[0], view.Buf), replace) + view.Cursor.ResetSelection() + messenger.Reset() + } else { + if view.Cursor.HasSelection() { + searchStart = ToCharPos(view.Cursor.CurSelection[1], view.Buf) + } else { + searchStart = ToCharPos(view.Cursor.Loc, view.Buf) + } + continue + } + } else { + view.Buf.Replace(FromCharPos(match[0], view.Buf), FromCharPos(match[1], view.Buf), replace) + } + } + view.Cursor.Relocate() + + if found > 1 { + messenger.Message("Replaced ", found, " occurences of ", search) + } else if found == 1 { + messenger.Message("Replaced ", found, " occurence of ", search) + } else { + messenger.Message("Nothing matched ", search) + } +} + // RunShellCommand executes a shell command and returns the output/error func RunShellCommand(input string) (string, error) { inputCmd := strings.Split(input, " ")[0] @@ -88,117 +379,9 @@ func HandleCommand(input string) { inputCmd := strings.Split(input, " ")[0] args := strings.Split(input, " ")[1:] - switch inputCmd { - case "set": - // Set an option and we have to set it for every view - for _, view := range views { - SetOption(view, args) - } - case "run": - // Run a shell command in the background (openTerm is false) - HandleShellCommand(strings.Join(args, " "), false) - case "quit": - // This is a bit weird because micro only has one view for now so there is no way to close - // a single view - // Currently if multiple views were open, it would close all of them, and not check the non-mainviews - // for unsaved changes. This, and the behavior of Ctrl-Q need to be changed when splits are implemented - if views[mainView].CanClose("Quit anyway? (yes, no, save) ") { - screen.Fini() - os.Exit(0) - } - case "save": - // Save the main view - views[mainView].Save() - case "replace": - // This is a regex to parse the replace expression - // We allow no quotes if there are no spaces, but if you want to search - // for or replace an expression with spaces, you can add double quotes - r := regexp.MustCompile(`"[^"\\]*(?:\\.[^"\\]*)*"|[^\s]*`) - replaceCmd := r.FindAllString(strings.Join(args, " "), -1) - if len(replaceCmd) < 2 { - // We need to find both a search and replace expression - messenger.Error("Invalid replace statement: " + strings.Join(args, " ")) - return - } - - var flags string - if len(replaceCmd) == 3 { - // The user included some flags - flags = replaceCmd[2] - } - - search := string(replaceCmd[0]) - replace := string(replaceCmd[1]) - - // If the search and replace expressions have quotes, we need to remove those - if strings.HasPrefix(search, `"`) && strings.HasSuffix(search, `"`) { - search = search[1 : len(search)-1] - } - if strings.HasPrefix(replace, `"`) && strings.HasSuffix(replace, `"`) { - replace = replace[1 : len(replace)-1] - } - - // We replace all escaped double quotes to real double quotes - search = strings.Replace(search, `\"`, `"`, -1) - replace = strings.Replace(replace, `\"`, `"`, -1) - // Replace some things so users can actually insert newlines and tabs in replacements - replace = strings.Replace(replace, "\\n", "\n", -1) - replace = strings.Replace(replace, "\\t", "\t", -1) - - regex, err := regexp.Compile(search) - if err != nil { - // There was an error with the user's regex - messenger.Error(err.Error()) - return - } - - view := views[mainView] - - found := false - for { - match := regex.FindStringIndex(view.Buf.String()) - if match == nil { - break - } - found = true - if strings.Contains(flags, "c") { - // The 'check' flag was used - Search(search, view, true) - view.Relocate() - if settings["syntax"].(bool) { - view.matches = Match(view) - } - RedrawAll() - choice, canceled := messenger.YesNoPrompt("Perform replacement? (y,n)") - if canceled { - if view.Cursor.HasSelection() { - view.Cursor.SetLoc(view.Cursor.CurSelection[0]) - view.Cursor.ResetSelection() - } - messenger.Reset() - return - } - if choice { - view.Cursor.DeleteSelection() - view.Buf.Insert(match[0], replace) - view.Cursor.ResetSelection() - messenger.Reset() - } else { - if view.Cursor.HasSelection() { - searchStart = view.Cursor.CurSelection[1] - } else { - searchStart = ToCharPos(view.Cursor.X, view.Cursor.Y, view.Buf) - } - continue - } - } else { - view.Buf.Replace(match[0], match[1], replace) - } - } - if !found { - messenger.Message("Nothing matched " + search) - } - default: - messenger.Error("Unknown command: " + inputCmd) + if _, ok := commands[inputCmd]; !ok { + messenger.Error("Unkown command ", inputCmd) + } else { + commands[inputCmd].action(args) } }