From 8a58506c72fa975738621bb4f8f0ac64bebb253c Mon Sep 17 00:00:00 2001 From: Zachary Yedidia Date: Wed, 17 Aug 2016 13:49:37 -0400 Subject: [PATCH] Add much better autocompletion for commands --- cmd/micro/actions.go | 2 +- cmd/micro/autocomplete.go | 89 +++++++++++++++++++++++++++++++++++++++ cmd/micro/command.go | 50 +++++++++++++--------- cmd/micro/messenger.go | 72 +++++++++++++++---------------- cmd/micro/runtime.go | 4 +- runtime/plugins/go/go.lua | 4 +- 6 files changed, 160 insertions(+), 61 deletions(-) create mode 100644 cmd/micro/autocomplete.go diff --git a/cmd/micro/actions.go b/cmd/micro/actions.go index 5d50055e..5bd99299 100644 --- a/cmd/micro/actions.go +++ b/cmd/micro/actions.go @@ -1119,7 +1119,7 @@ func (v *View) CommandMode() bool { return false } - input, canceled := messenger.Prompt("> ", "Command", FileCompletion) + input, canceled := messenger.Prompt("> ", "Command", CommandCompletion) if !canceled { HandleCommand(input) PostActionCall("CommandMode") diff --git a/cmd/micro/autocomplete.go b/cmd/micro/autocomplete.go new file mode 100644 index 00000000..61f00f11 --- /dev/null +++ b/cmd/micro/autocomplete.go @@ -0,0 +1,89 @@ +package main + +import ( + "io/ioutil" + "os" + "strings" +) + +func FileComplete(input string) (string, []string) { + dirs := strings.Split(input, "/") + var files []os.FileInfo + var err error + if len(dirs) > 1 { + files, err = ioutil.ReadDir(strings.Join(dirs[:len(dirs)-1], "/")) + } else { + files, err = ioutil.ReadDir(".") + } + var suggestions []string + if err != nil { + return "", suggestions + } + for _, f := range files { + name := f.Name() + if f.IsDir() { + name += "/" + } + if strings.HasPrefix(name, dirs[len(dirs)-1]) { + suggestions = append(suggestions, name) + } + } + + var chosen string + if len(suggestions) == 1 { + if len(dirs) > 1 { + chosen = strings.Join(dirs[:len(dirs)-1], "/") + "/" + suggestions[0] + } else { + chosen = suggestions[0] + } + } + + return chosen, suggestions +} + +func CommandComplete(input string) (string, []string) { + var suggestions []string + for cmd := range commands { + if strings.HasPrefix(cmd, input) { + suggestions = append(suggestions, cmd) + } + } + + var chosen string + if len(suggestions) == 1 { + chosen = suggestions[0] + } + return chosen, suggestions +} + +func HelpComplete(input string) (string, []string) { + var suggestions []string + + for _, topic := range helpFiles { + if strings.HasPrefix(topic, input) { + + suggestions = append(suggestions, topic) + } + } + + var chosen string + if len(suggestions) == 1 { + chosen = suggestions[0] + } + return chosen, suggestions +} + +func OptionComplete(input string) (string, []string) { + var suggestions []string + for option := range settings { + if strings.HasPrefix(option, input) { + suggestions = append(suggestions, option) + } + } + + var chosen string + if len(suggestions) == 1 { + chosen = suggestions[0] + } + return chosen, suggestions +} diff --git a/cmd/micro/command.go b/cmd/micro/command.go index 0a16815b..fa593ff0 100644 --- a/cmd/micro/command.go +++ b/cmd/micro/command.go @@ -12,7 +12,17 @@ import ( "github.com/mitchellh/go-homedir" ) -var commands map[string]func([]string) +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, @@ -29,21 +39,21 @@ var commandActions = map[string]func([]string){ // InitCommands initializes the default commands func InitCommands() { - commands = make(map[string]func([]string)) + commands = make(map[string]Command) defaults := DefaultCommands() parseCommands(defaults) } -func parseCommands(userCommands map[string]string) { +func parseCommands(userCommands map[string]StrCommand) { for k, v := range userCommands { - MakeCommand(k, v) + 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) { +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 @@ -51,22 +61,22 @@ func MakeCommand(name, function string) { action = LuaFunctionCommand(function) } - commands[name] = action + commands[name] = Command{action, completions} } // DefaultCommands returns a map containing micro's default commands -func DefaultCommands() map[string]string { - return map[string]string{ - "set": "Set", - "bind": "Bind", - "run": "Run", - "quit": "Quit", - "save": "Save", - "replace": "Replace", - "vsplit": "VSplit", - "hsplit": "HSplit", - "tab": "Tab", - "help": "Help", +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}}, } } @@ -129,7 +139,7 @@ func HSplit(args []string) { } } -// Tab opens the given file in a new tab +// NewTab opens the given file in a new tab func NewTab(args []string) { if len(args) == 0 { CurView().AddTab() @@ -370,6 +380,6 @@ func HandleCommand(input string) { if _, ok := commands[inputCmd]; !ok { messenger.Error("Unkown command ", inputCmd) } else { - commands[inputCmd](args) + commands[inputCmd].action(args) } } diff --git a/cmd/micro/messenger.go b/cmd/micro/messenger.go index 36495501..6fe4c5bf 100644 --- a/cmd/micro/messenger.go +++ b/cmd/micro/messenger.go @@ -4,7 +4,6 @@ import ( "bufio" "bytes" "fmt" - "io/ioutil" "os" "strconv" "strings" @@ -129,11 +128,14 @@ type Completion int const ( NoCompletion Completion = iota FileCompletion + CommandCompletion + HelpCompletion + OptionCompletion ) // Prompt sends the user a message and waits for a response to be typed in // This function blocks the main loop while waiting for input -func (m *Messenger) Prompt(prompt, historyType string, completionType Completion) (string, bool) { +func (m *Messenger) Prompt(prompt, historyType string, completionTypes ...Completion) (string, bool) { m.hasPrompt = true m.Message(prompt) if _, ok := m.history[historyType]; !ok { @@ -163,42 +165,40 @@ func (m *Messenger) Prompt(prompt, historyType string, completionType Completion response, canceled = m.response, false m.history[historyType][len(m.history[historyType])-1] = response case tcell.KeyTab: + args := strings.Split(m.response, " ") + currentArgNum := len(args) - 1 + currentArg := args[currentArgNum] + var completionType Completion + + if completionTypes[0] == CommandCompletion && len(completionTypes) == 1 && currentArgNum > 0 { + if command, ok := commands[args[0]]; ok { + completionTypes = append(completionTypes, command.completions...) + } + } + + if currentArgNum >= len(completionTypes) { + completionType = completionTypes[len(completionTypes)-1] + } else { + completionType = completionTypes[currentArgNum] + } + + var chosen string if completionType == FileCompletion { - args := strings.Split(m.response, " ") - currentArg := args[len(args)-1] - dirs := strings.Split(currentArg, "/") - var files []os.FileInfo - var err error - if len(dirs) > 1 { - files, err = ioutil.ReadDir(strings.Join(dirs[:len(dirs)-1], "/")) - } else { - files, err = ioutil.ReadDir(".") - } - if err != nil { - continue - } - var suggestions []string - for _, f := range files { - name := f.Name() - if f.IsDir() { - name += "/" - } - if strings.HasPrefix(name, dirs[len(dirs)-1]) { - suggestions = append(suggestions, name) - } - } - if len(suggestions) == 1 { - if len(dirs) > 1 { - currentArg = strings.Join(dirs[:len(dirs)-1], "/") + "/" + suggestions[0] - } else { - currentArg = suggestions[0] - } - if len(args) > 1 { - currentArg = " " + currentArg - } - m.response = strings.Join(args[:len(args)-1], " ") + currentArg - m.cursorx = Count(m.response) + chosen, _ = FileComplete(currentArg) + } else if completionType == CommandCompletion { + chosen, _ = CommandComplete(currentArg) + } else if completionType == HelpCompletion { + chosen, _ = HelpComplete(currentArg) + } else if completionType == OptionCompletion { + chosen, _ = OptionComplete(currentArg) + } + + if chosen != "" { + if len(args) > 1 { + chosen = " " + chosen } + m.response = strings.Join(args[:len(args)-1], " ") + chosen + m.cursorx = Count(m.response) } } } diff --git a/cmd/micro/runtime.go b/cmd/micro/runtime.go index d7f1d7f5..2884dad3 100644 --- a/cmd/micro/runtime.go +++ b/cmd/micro/runtime.go @@ -431,7 +431,7 @@ func runtimePluginsAutocloseAutocloseLua() (*asset, error) { return a, nil } -var _runtimePluginsGoGoLua = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xa4\x52\x5d\x8b\xd4\x30\x14\x7d\xef\xaf\x38\x04\x84\x44\xbb\xfd\x01\x85\x3e\xe8\x82\xfb\x24\x2b\xba\xf8\x22\x0a\xb1\xbd\x6d\x83\x69\x52\x92\xdb\x1d\x45\xfc\xef\xd2\x4e\xb6\xd3\xe9\x8c\x8a\x98\x97\x42\x72\xee\xf9\xb8\x3d\xa6\xc5\x1d\xf1\xfd\xc8\xc6\x3b\x29\x3a\x6f\x86\xd1\x07\x8e\x42\xa1\xaa\xe0\x8c\x05\xf7\xe4\x32\x00\x78\xd9\x34\x97\xb0\x1c\xad\xb6\x91\x54\x46\xae\xc9\xf6\x5c\xed\xc0\x7f\xe3\x99\x21\x39\x38\x4c\x89\x22\x7b\xa3\xbf\xd2\xad\x1f\x06\xed\x9a\x73\x1d\xd1\xf9\x22\xea\x47\x12\x6a\x8f\x39\x72\x6c\xde\xb3\x76\x72\xf5\xac\x00\xef\xde\xeb\x47\x92\x6a\x11\x36\x2d\x6e\xa7\xf0\xc1\xd0\x41\xaa\xe2\xd5\xd4\x16\xaf\x8d\xa5\x87\xef\x23\xcd\x16\xc5\x9d\x17\x27\x8f\x09\xfe\x9b\xcd\x9c\xc1\xe6\xb3\x3e\x26\xa5\xf9\x90\x8d\x74\x7d\x21\x57\xc6\xdb\x81\xb7\xa3\xae\xc9\x9e\xbe\xcb\x52\xd6\x3c\x5b\xa4\xf5\xb5\xb6\xe8\xb5\x6b\x2c\xa1\x82\xf1\xc5\xe8\x47\x7a\xd2\xc1\xcd\x01\x02\x45\xb1\x8b\xfc\x56\x73\xbf\x1d\x0f\x14\x27\xcb\xa8\x12\x4f\x19\x48\x37\x52\x3c\xd7\xe2\x08\x4a\xb7\xb5\xf5\x71\xde\xe2\x72\xb7\x12\x96\xef\xe8\x7e\x16\x54\x17\x26\xcf\xb7\xf1\x07\xa3\x09\xf8\xaf\x66\xe3\x68\x0d\xcb\x0b\xcb\x39\x44\xf9\x3f\xc6\xe3\xa9\x2b\x37\x37\x78\xe8\x4d\xc4\xc1\x58\x0b\x0e\xa6\xeb\x28\xa4\x36\x41\xbb\x06\xb5\x9e\x22\x1d\x7f\x07\x7c\x38\x45\x06\x7b\x84\xc9\xed\xf4\x52\x09\x77\x6a\x4b\x88\xc8\x21\x47\xa4\xf1\x6a\xce\x1f\x3f\xcf\x6e\x3b\xfa\x86\x0a\x52\xc8\x8f\x9f\x9f\xc5\x4f\x2f\x94\x50\x65\xeb\xc3\xa0\x59\xae\x04\xad\x0f\x20\x5d\xf7\x30\x0e\x91\x43\xd9\x0d\x9a\xeb\x5e\x2e\xb3\x0a\x8d\x5f\x3b\xc6\xfa\x8b\xa5\xc2\xb8\x48\x81\xe5\x51\x30\x5f\x26\x55\xb6\xad\x60\x20\x9e\x82\x4b\x8e\x96\x00\xbf\x02\x00\x00\xff\xff\x4f\x90\xa7\x78\x31\x04\x00\x00") +var _runtimePluginsGoGoLua = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xa4\x52\x5d\x8b\xd4\x30\x14\x7d\xef\xaf\x38\x04\x84\x44\x3b\xc5\xe7\x81\x3e\xe8\x82\xfb\x24\x2b\xba\xf8\x22\x0a\xb1\xbd\x6d\x83\x69\x52\x92\xdb\x1d\x45\xfc\xef\xd2\x4e\xb6\x33\xed\x8c\x8a\x6c\x5e\x06\xee\x9c\x7b\x3e\x6e\x8f\x69\x70\x4b\x7c\x37\xb0\xf1\x4e\x8a\xd6\x9b\x7e\xf0\x81\xa3\x50\x28\x4b\x38\x63\xc1\x1d\xb9\x0c\x00\x5e\xd5\xf5\x25\x2c\x47\xa3\x6d\x24\x95\x91\xab\xb3\x2d\x57\xd3\xf3\xbf\x78\x26\x48\x0e\x0e\x63\xa2\xc8\xde\xea\x6f\x74\xe3\xfb\x5e\xbb\x7a\xad\x23\x5a\x5f\x44\xfd\x40\x22\xc7\x4b\xb5\x85\x1d\x69\xd6\x90\xac\x19\x5d\x35\xe9\xc0\xbb\x0f\xfa\x81\xa4\x9a\xe5\x4d\x83\x9b\x31\x7c\x34\x74\x90\xaa\x78\x3d\x36\xc5\x1b\x63\xe9\xfe\xc7\x40\x93\x51\x71\xeb\xc5\xc9\x69\x82\xff\xe1\x3e\x2b\xd8\xf4\x96\x3f\x93\xd2\xf4\xc8\x46\xba\x7e\x96\x2b\xeb\x4d\xcf\xe7\xab\xae\xce\x1e\x7f\xe7\xd3\x2c\x79\xce\x91\xd6\x57\xda\xa2\xd3\xae\xb6\x84\x12\xc6\x17\x83\x1f\xe8\x51\x07\xbb\x03\x04\x8a\x62\x13\xf9\x9d\xe6\xee\x7c\x3d\x50\x1c\x2d\xa3\x4c\x3c\xfb\x40\xba\x96\xe2\xb9\x16\x47\x50\x9a\x56\xd6\xc7\xe9\x8a\xf3\x6c\x21\xdc\xbf\xa7\xbb\x49\x50\x5d\x98\x5c\x5f\xe3\x2f\x46\x13\xf0\x7f\xcd\xc6\xc1\x1a\x96\x17\x96\x73\x88\xfd\x53\x8c\xc7\x53\x57\x76\x3b\xdc\x77\x26\xe2\x60\xac\x05\x07\xd3\xb6\x14\x52\x9b\xa0\x5d\x8d\x4a\x8f\x91\x8e\x9f\x03\x3e\x9c\x22\x83\x3d\xc2\xe8\x36\x7a\xa9\x84\x1b\xb5\x39\x44\xe4\x90\x23\xd2\x70\x35\xe7\xcf\x5f\xab\x69\x4b\xdf\x51\x42\x0a\xf9\xe9\xcb\xb3\xf8\xf9\x85\x12\x6a\xdf\xf8\xd0\x6b\x96\x0b\x41\xe3\x03\x48\x57\x1d\x8c\x43\xe4\xb0\x6f\x7b\xcd\x55\x27\xe7\x5d\x85\xda\x2f\x1d\x63\xfd\xd5\x52\x61\x5c\xa4\xc0\xf2\x28\x98\xcf\x9b\x2a\x3b\xaf\x60\x20\x1e\x83\x4b\x8e\xe6\x00\xbf\x03\x00\x00\xff\xff\xe8\x9d\x9e\x71\x37\x04\x00\x00") func runtimePluginsGoGoLuaBytes() ([]byte, error) { return bindataRead( @@ -446,7 +446,7 @@ func runtimePluginsGoGoLua() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "runtime/plugins/go/go.lua", size: 1073, mode: os.FileMode(420), modTime: time.Unix(1471450014, 0)} + info := bindataFileInfo{name: "runtime/plugins/go/go.lua", size: 1079, mode: os.FileMode(420), modTime: time.Unix(1471455442, 0)} a := &asset{bytes: bytes, info: info} return a, nil } diff --git a/runtime/plugins/go/go.lua b/runtime/plugins/go/go.lua index 0ec81a40..e62130d1 100644 --- a/runtime/plugins/go/go.lua +++ b/runtime/plugins/go/go.lua @@ -5,8 +5,8 @@ if GetOption("gofmt") == nil then AddOption("gofmt", true) end -MakeCommand("goimports", "go.save") -MakeCommand("gofmt", "go.save") +MakeCommand("goimports", "go.save", 0) +MakeCommand("gofmt", "go.save", 0) function onSave() if CurView().Buf.FileType == "Go" then