X-Git-Url: https://git.lizzy.rs/?a=blobdiff_plain;f=cmd%2Fmicro%2Fcommand.go;h=b41a3db4748920cbaacf7902524817d3c6c585e8;hb=f9cb99b35fa36099ac254e129e2668ada6a5034f;hp=eb610e887f33c0a7964b35a6e9fa1ed7c04e2504;hpb=838a932dd96a709c37fbee7c0117ad4b29356d28;p=micro.git diff --git a/cmd/micro/command.go b/cmd/micro/command.go index eb610e88..b41a3db4 100644 --- a/cmd/micro/command.go +++ b/cmd/micro/command.go @@ -2,194 +2,386 @@ package main import ( "bytes" + "io/ioutil" "os" "os/exec" + "os/signal" "regexp" "strings" - "github.com/gdamore/tcell" + "github.com/mitchellh/go-homedir" ) -// HandleShellCommand runs the shell command and outputs to DisplayBlock -func HandleShellCommand(input string, view *View) { - inputCmd := strings.Split(input, " ")[0] - args := strings.Split(input, " ")[1:] - if inputCmd == "exit" { - messenger.Message("Ctrl+Q to exit") - return - } - if inputCmd == "help" { - DisplayHelp() - return +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...) } - cmd := exec.Command(inputCmd, args...) - var stdout bytes.Buffer - var stderr bytes.Buffer - cmd.Stdout = &stdout // send output to buffer - cmd.Stderr = &stderr // send error to a different buffer - // Execute Command - err := cmd.Run() - if err != nil { - messenger.Message(err.Error() + "... " + stderr.String()) - return +} + +// 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) } - outstring := stdout.String() - totalLines := strings.Split(outstring, "\n") + commands[name] = Command{action, completions} +} - if len(totalLines) < 3 { - messenger.Message(outstring) - return +// 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}}, } +} - if outstring != "" { - DisplayBlock(outstring) +// 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) + } } } -// DisplayBlock displays txt -// It blocks the main loop -func DisplayBlock(text string) { - topline := 0 - _, height := screen.Size() - screen.HideCursor() - totalLines := strings.Split(text, "\n") - for { - screen.Clear() +// 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) - lineEnd := topline + height - if lineEnd > len(totalLines) { - lineEnd = len(totalLines) + 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) } - lines := totalLines[topline:lineEnd] - for y, line := range lines { - for x, ch := range line { - st := defStyle - screen.SetContent(x, y, ch, nil, st) - } + 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) + } +} - screen.Show() - - event := screen.PollEvent() - switch e := event.(type) { - case *tcell.EventResize: - _, height = e.Size() - case *tcell.EventKey: - switch e.Key() { - case tcell.KeyPgUp: - if topline > height { - topline = topline - height - } else { - topline = 0 - } - case tcell.KeyPgDn: - if topline < len(totalLines)-height { - topline = topline + height - } - case tcell.KeyUp: - if topline > 0 { - topline-- - } - case tcell.KeyDown: - if topline < len(totalLines)-height { - topline++ +// 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() } - case tcell.KeyCtrlQ, tcell.KeyCtrlW, tcell.KeyEscape, tcell.KeyCtrlC: - return - default: - return } } } } -// HandleCommand handles input from the user -func HandleCommand(input string, view *View) { - inputCmd := strings.Split(input, " ")[0] - args := strings.Split(input, " ")[1:] +// Set sets an option +func Set(args []string) { + if len(args) < 2 { + return + } - commands := []string{"set", "quit", "save", "replace"} + option := strings.TrimSpace(args[0]) + value := strings.TrimSpace(args[1]) - i := 0 - cmd := inputCmd + SetOptionAndSettings(option, value) +} - for _, c := range commands { - if strings.HasPrefix(c, inputCmd) { - i++ - cmd = c - } +// 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 } - if i == 1 { - inputCmd = cmd + + var flags string + if len(replaceCmd) == 3 { + // The user included some flags + flags = replaceCmd[2] } - switch inputCmd { - case "set": - SetOption(view, args) - case "quit": - if view.CanClose("Quit anyway? (yes, no, save) ") { - screen.Fini() - os.Exit(0) - } - case "save": - view.Save() - case "replace": - r := regexp.MustCompile(`"[^"\\]*(?:\\.[^"\\]*)*"|[^\s]*`) - replaceCmd := r.FindAllString(strings.Join(args, " "), -1) - if len(replaceCmd) < 2 { - messenger.Error("Invalid replace statement: " + strings.Join(args, " ")) - return - } + search := string(replaceCmd[0]) + replace := string(replaceCmd[1]) - var flags string - if len(replaceCmd) == 3 { - // The user included some flags - flags = replaceCmd[2] - } + // 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] + } - search := string(replaceCmd[0]) - replace := string(replaceCmd[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) - if strings.HasPrefix(search, `"`) && strings.HasSuffix(search, `"`) { - search = search[1 : len(search)-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 } - if strings.HasPrefix(replace, `"`) && strings.HasSuffix(replace, `"`) { - replace = replace[1 : len(replace)-1] + 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() - search = strings.Replace(search, `\"`, `"`, -1) - replace = strings.Replace(replace, `\"`, `"`, -1) + 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) + } +} - // messenger.Error(search + " -> " + replace) +// RunShellCommand executes a shell command and returns the output/error +func RunShellCommand(input string) (string, error) { + inputCmd := strings.Split(input, " ")[0] + args := strings.Split(input, " ")[1:] - regex, err := regexp.Compile(search) - if err != nil { - messenger.Error(err.Error()) - return - } + cmd := exec.Command(inputCmd, args...) + outputBytes := &bytes.Buffer{} + cmd.Stdout = outputBytes + cmd.Stderr = outputBytes + cmd.Start() + err := cmd.Wait() // wait for command to finish + outstring := outputBytes.String() + return outstring, err +} - found := false - for { - match := regex.FindStringIndex(view.buf.text) - if match == nil { - break +// HandleShellCommand runs the shell command +// The openTerm argument specifies whether a terminal should be opened (for viewing output +// or interacting with stdin) +func HandleShellCommand(input string, openTerm bool) { + inputCmd := strings.Split(input, " ")[0] + if !openTerm { + // Simply run the command in the background and notify the user when it's done + messenger.Message("Running...") + go func() { + output, err := RunShellCommand(input) + totalLines := strings.Split(output, "\n") + + if len(totalLines) < 3 { + if err == nil { + messenger.Message(inputCmd, " exited without error") + } else { + messenger.Message(inputCmd, " exited with error: ", err, ": ", output) + } + } else { + messenger.Message(output) } - found = true - if strings.Contains(flags, "c") { - // // The 'check' flag was used - // if messenger.YesNoPrompt("Perform replacement?") { - // view.eh.Replace(match[0], match[1], replace) - // } else { - // continue - // } + // We have to make sure to redraw + RedrawAll() + }() + } else { + // Shut down the screen because we're going to interact directly with the shell + screen.Fini() + screen = nil + + args := strings.Split(input, " ")[1:] + + // Set up everything for the command + cmd := exec.Command(inputCmd, args...) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + // This is a trap for Ctrl-C so that it doesn't kill micro + // Instead we trap Ctrl-C to kill the program we're running + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + go func() { + for range c { + cmd.Process.Kill() } - view.eh.Replace(match[0], match[1], replace) - } - if !found { - messenger.Message("Nothing matched " + search) - } - default: - messenger.Error("Unknown command: " + inputCmd) + }() + + // Start the command + cmd.Start() + cmd.Wait() + + // This is just so we don't return right away and let the user press enter to return + TermMessage("") + + // Start the screen back up + InitScreen() + } +} + +// HandleCommand handles input from the user +func HandleCommand(input string) { + inputCmd := strings.Split(input, " ")[0] + args := strings.Split(input, " ")[1:] + + if _, ok := commands[inputCmd]; !ok { + messenger.Error("Unkown command ", inputCmd) + } else { + commands[inputCmd].action(args) } }