diff --git a/cmd/micro/action/command.go b/cmd/micro/action/command.go index 30b4a4d8..e2db143d 100644 --- a/cmd/micro/action/command.go +++ b/cmd/micro/action/command.go @@ -18,109 +18,59 @@ import ( "github.com/zyedidia/micro/cmd/micro/util" ) -// A Command contains an action (a function to call) as well as information about how to autocomplete the command +// 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) - completions []Completion -} - -// A StrCommand is similar to a command but keeps the name of the action -type StrCommand struct { - action string - completions []Completion + action func(*BufPane, []string) + completer buffer.Completer } var commands map[string]Command -var commandActions = map[string]func(*BufPane, []string){ - "Set": (*BufPane).SetCmd, - "SetLocal": (*BufPane).SetLocalCmd, - "Show": (*BufPane).ShowCmd, - "ShowKey": (*BufPane).ShowKeyCmd, - "Run": (*BufPane).RunCmd, - "Bind": (*BufPane).BindCmd, - "Unbind": (*BufPane).UnbindCmd, - "Quit": (*BufPane).QuitCmd, - "Save": (*BufPane).SaveCmd, - "Replace": (*BufPane).ReplaceCmd, - "ReplaceAll": (*BufPane).ReplaceAllCmd, - "VSplit": (*BufPane).VSplitCmd, - "HSplit": (*BufPane).HSplitCmd, - "Tab": (*BufPane).NewTabCmd, - "Help": (*BufPane).HelpCmd, - "Eval": (*BufPane).EvalCmd, - "ToggleLog": (*BufPane).ToggleLogCmd, - "Plugin": (*BufPane).PluginCmd, - "Reload": (*BufPane).ReloadCmd, - "Cd": (*BufPane).CdCmd, - "Pwd": (*BufPane).PwdCmd, - "Open": (*BufPane).OpenCmd, - "TabSwitch": (*BufPane).TabSwitchCmd, - "Term": (*BufPane).TermCmd, - "MemUsage": (*BufPane).MemUsageCmd, - "Retab": (*BufPane).RetabCmd, - "Raw": (*BufPane).RawCmd, -} - -// 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...) + commands = map[string]Command{ + "set": Command{(*BufPane).SetCmd, nil}, + "setlocal": Command{(*BufPane).SetLocalCmd, nil}, + "show": Command{(*BufPane).ShowCmd, nil}, + "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}, + "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, nil}, + "eval": Command{(*BufPane).EvalCmd, nil}, + "togglelog": Command{(*BufPane).ToggleLogCmd, nil}, + "plugin": Command{(*BufPane).PluginCmd, nil}, + "reload": Command{(*BufPane).ReloadCmd, 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 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": {"Set", []Completion{OptionCompletion, OptionValueCompletion}}, - "setlocal": {"SetLocal", []Completion{OptionCompletion, OptionValueCompletion}}, - "show": {"Show", []Completion{OptionCompletion, NoCompletion}}, - "showkey": {"ShowKey", []Completion{NoCompletion}}, - "bind": {"Bind", []Completion{NoCompletion}}, - "unbind": {"Unbind", []Completion{NoCompletion}}, - "run": {"Run", []Completion{NoCompletion}}, - "quit": {"Quit", []Completion{NoCompletion}}, - "save": {"Save", []Completion{NoCompletion}}, - "replace": {"Replace", []Completion{NoCompletion}}, - "replaceall": {"ReplaceAll", []Completion{NoCompletion}}, - "vsplit": {"VSplit", []Completion{FileCompletion, NoCompletion}}, - "hsplit": {"HSplit", []Completion{FileCompletion, NoCompletion}}, - "tab": {"Tab", []Completion{FileCompletion, NoCompletion}}, - "help": {"Help", []Completion{HelpCompletion, NoCompletion}}, - "eval": {"Eval", []Completion{NoCompletion}}, - "log": {"ToggleLog", []Completion{NoCompletion}}, - "plugin": {"Plugin", []Completion{PluginCmdCompletion, PluginNameCompletion}}, - "reload": {"Reload", []Completion{NoCompletion}}, - "cd": {"Cd", []Completion{FileCompletion}}, - "pwd": {"Pwd", []Completion{NoCompletion}}, - "open": {"Open", []Completion{FileCompletion}}, - "tabswitch": {"TabSwitch", []Completion{NoCompletion}}, - "term": {"Term", []Completion{NoCompletion}}, - "memusage": {"MemUsage", []Completion{NoCompletion}}, - "retab": {"Retab", []Completion{NoCompletion}}, - "raw": {"Raw", []Completion{NoCompletion}}, - } -} +// 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} +// } // CommandEditAction returns a bindable function that opens a prompt with // the given string and executes the command when the user presses diff --git a/cmd/micro/action/infocomplete.go b/cmd/micro/action/infocomplete.go index f25a3a9b..46acf21f 100644 --- a/cmd/micro/action/infocomplete.go +++ b/cmd/micro/action/infocomplete.go @@ -1,10 +1,11 @@ package action import ( - "io/ioutil" - "os" + "bytes" "strings" + "unicode/utf8" + "github.com/zyedidia/micro/cmd/micro/buffer" "github.com/zyedidia/micro/cmd/micro/config" "github.com/zyedidia/micro/cmd/micro/util" ) @@ -29,54 +30,22 @@ var pluginCompletions []func(string) []string // while coding. This helps micro autocomplete commands and then filenames // for example with `vsplit filename`. -// FileComplete autocompletes filenames -func FileComplete(input string) (string, []string) { - var sep string = string(os.PathSeparator) - dirs := strings.Split(input, sep) - - var files []os.FileInfo - var err error - if len(dirs) > 1 { - directories := strings.Join(dirs[:len(dirs)-1], sep) + sep - - directories, _ = util.ReplaceHome(directories) - files, err = ioutil.ReadDir(directories) - } else { - files, err = ioutil.ReadDir(".") - } - - var suggestions []string - if err != nil { - return "", suggestions - } - for _, f := range files { - name := f.Name() - if f.IsDir() { - name += sep - } - 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], sep) + sep + suggestions[0] - } else { - chosen = suggestions[0] - } - } else { - if len(dirs) > 1 { - chosen = strings.Join(dirs[:len(dirs)-1], sep) + sep - } - } - - return chosen, suggestions -} - // CommandComplete autocompletes commands -func CommandComplete(input string) (string, []string) { +func CommandComplete(b *buffer.Buffer) (string, []string) { + c := b.GetActiveCursor() + l := b.LineBytes(c.Y) + l = util.SliceStart(l, c.X) + + args := bytes.Split(l, []byte{' '}) + input := string(args[len(args)-1]) + argstart := 0 + for i, a := range args { + if i == len(args)-1 { + break + } + argstart += utf8.RuneCount(a) + 1 + } + var suggestions []string for cmd := range commands { if strings.HasPrefix(cmd, input) { @@ -86,7 +55,7 @@ func CommandComplete(input string) (string, []string) { var chosen string if len(suggestions) == 1 { - chosen = suggestions[0] + chosen = util.SliceEndStr(suggestions[0], c.X-argstart) } return chosen, suggestions } diff --git a/cmd/micro/action/infopane.go b/cmd/micro/action/infopane.go index d71371c3..c42bed30 100644 --- a/cmd/micro/action/infopane.go +++ b/cmd/micro/action/infopane.go @@ -1,8 +1,12 @@ package action import ( + "bytes" + "log" + "github.com/zyedidia/micro/cmd/micro/display" "github.com/zyedidia/micro/cmd/micro/info" + "github.com/zyedidia/micro/cmd/micro/util" "github.com/zyedidia/tcell" ) @@ -161,6 +165,32 @@ func (h *InfoPane) CursorDown() { } func (h *InfoPane) InsertTab() { // TODO: autocomplete + b := h.Buf + c := b.GetActiveCursor() + l := b.LineBytes(0) + l = util.SliceStart(l, c.X) + + args := bytes.Split(l, []byte{' '}) + cmd := string(args[0]) + + var ins string + var suggestions []string + if len(args) == 1 { + ins, suggestions = CommandComplete(b) + } else { + if action, ok := commands[cmd]; ok { + if action.completer != nil { + ins, suggestions = action.completer(b) + } + } + } + log.Println(ins, suggestions) + + if len(suggestions) == 1 { + b.Insert(c.Loc, ins) + } else if len(suggestions) > 1 { + h.MakeSuggestions(suggestions) + } } func (h *InfoPane) InsertNewline() { if !h.HasYN { diff --git a/cmd/micro/buffer/autocomplete.go b/cmd/micro/buffer/autocomplete.go new file mode 100644 index 00000000..34c3fd07 --- /dev/null +++ b/cmd/micro/buffer/autocomplete.go @@ -0,0 +1,82 @@ +package buffer + +import ( + "bytes" + "io/ioutil" + "os" + "strings" + "unicode/utf8" + + "github.com/zyedidia/micro/cmd/micro/util" +) + +type Completer func(*Buffer) (string, []string) + +func (b *Buffer) GetSuggestions() { + +} + +func (b *Buffer) Autocomplete(c Completer) { + +} + +// FileComplete autocompletes filenames +func FileComplete(b *Buffer) (string, []string) { + c := b.GetActiveCursor() + l := b.LineBytes(c.Y) + l = util.SliceStart(l, c.X) + + args := bytes.Split(l, []byte{' '}) + input := string(args[len(args)-1]) + argstart := 0 + for i, a := range args { + if i == len(args)-1 { + break + } + argstart += utf8.RuneCount(a) + 1 + } + + sep := string(os.PathSeparator) + dirs := strings.Split(input, sep) + + var files []os.FileInfo + var err error + if len(dirs) > 1 { + directories := strings.Join(dirs[:len(dirs)-1], sep) + sep + + directories, _ = util.ReplaceHome(directories) + files, err = ioutil.ReadDir(directories) + } else { + files, err = ioutil.ReadDir(".") + } + + var suggestions []string + if err != nil { + return "", suggestions + } + for _, f := range files { + name := f.Name() + if f.IsDir() { + name += sep + } + 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], sep) + sep + suggestions[0] + } else { + chosen = suggestions[0] + } + } else { + if len(dirs) > 1 { + chosen = strings.Join(dirs[:len(dirs)-1], sep) + sep + } + } + chosen = util.SliceEndStr(chosen, c.X-argstart) + + return chosen, suggestions +} diff --git a/cmd/micro/display/infowindow.go b/cmd/micro/display/infowindow.go index 8b74f5f1..111bfcd0 100644 --- a/cmd/micro/display/infowindow.go +++ b/cmd/micro/display/infowindow.go @@ -147,18 +147,19 @@ func (i *InfoWindow) displayBuffer() { } } +var keydisplay = []string{"^Q Quit, ^S Save, ^O Open, ^G Help, ^E Command Bar, ^K Cut Line", "^F Find, ^Z Undo, ^Y Redo, ^A Select All, ^D Duplicate Line, ^T New Tab"} + func (i *InfoWindow) displayKeyMenu() { // TODO: maybe make this based on the actual keybindings - display := []string{"^Q Quit, ^S Save, ^O Open, ^G Help, ^E Command Bar, ^K Cut Line", "^F Find, ^Z Undo, ^Y Redo, ^A Select All, ^D Duplicate Line, ^T New Tab"} - log.Println("hi", len(display), i.Width) - for y := 0; y < len(display); y++ { + log.Println("hi", len(keydisplay), i.Width) + for y := 0; y < len(keydisplay); y++ { for x := 0; x < i.Width; x++ { - log.Println(x, i.Y-len(display)+y) - if x < len(display[y]) { - screen.Screen.SetContent(x, i.Y-len(display)+y, rune(display[y][x]), nil, config.DefStyle) + log.Println(x, i.Y-len(keydisplay)+y) + if x < len(keydisplay[y]) { + screen.Screen.SetContent(x, i.Y-len(keydisplay)+y, rune(keydisplay[y][x]), nil, config.DefStyle) } else { - screen.Screen.SetContent(x, i.Y-len(display)+y, ' ', nil, config.DefStyle) + screen.Screen.SetContent(x, i.Y-len(keydisplay)+y, ' ', nil, config.DefStyle) } } } @@ -191,4 +192,36 @@ func (i *InfoWindow) Display() { i.displayBuffer() } } + + if i.HasSuggestions { + i.HasSuggestions = false + statusLineStyle := config.DefStyle.Reverse(true) + if style, ok := config.Colorscheme["statusline"]; ok { + statusLineStyle = style + } + keymenuOffset := 0 + if config.GetGlobalOption("keymenu").(bool) { + keymenuOffset = len(keydisplay) + } + x := 0 + for _, s := range i.Suggestions { + for _, r := range s { + screen.Screen.SetContent(x, i.Y-keymenuOffset-1, r, nil, statusLineStyle) + x++ + if x >= i.Width { + return + } + } + screen.Screen.SetContent(x, i.Y-keymenuOffset-1, ' ', nil, statusLineStyle) + x++ + if x >= i.Width { + return + } + } + + for x < i.Width { + screen.Screen.SetContent(x, i.Y-keymenuOffset-1, ' ', nil, statusLineStyle) + x++ + } + } } diff --git a/cmd/micro/info/infobuffer.go b/cmd/micro/info/infobuffer.go index aebdd347..3ea3c8b0 100644 --- a/cmd/micro/info/infobuffer.go +++ b/cmd/micro/info/infobuffer.go @@ -16,6 +16,9 @@ type InfoBuf struct { HasError bool HasYN bool + HasSuggestions bool + Suggestions []string + PromptType string Msg string @@ -159,3 +162,8 @@ func (i *InfoBuf) Reset() { i.HasPrompt, i.HasMessage, i.HasError = false, false, false i.HasGutter = false } + +func (i *InfoBuf) MakeSuggestions(s []string) { + i.HasSuggestions = true + i.Suggestions = s +} diff --git a/cmd/micro/util/util.go b/cmd/micro/util/util.go index 53cac87a..0d23028e 100644 --- a/cmd/micro/util/util.go +++ b/cmd/micro/util/util.go @@ -33,6 +33,24 @@ func SliceEnd(slc []byte, index int) []byte { return slc[totalSize:] } +// SliceEndStr is the same as SliceEnd but for strings +func SliceEndStr(str string, index int) string { + len := len(str) + i := 0 + totalSize := 0 + for totalSize < len { + if i >= index { + return str[totalSize:] + } + + _, size := utf8.DecodeRuneInString(str[totalSize:]) + totalSize += size + i++ + } + + return str[totalSize:] +} + // SliceStart returns a byte slice where the index is a rune index // Slices off the end of the slice func SliceStart(slc []byte, index int) []byte { @@ -52,6 +70,24 @@ func SliceStart(slc []byte, index int) []byte { return slc[:totalSize] } +// SliceStartStr is the same as SliceStart but for strings +func SliceStartStr(str string, index int) string { + len := len(str) + i := 0 + totalSize := 0 + for totalSize < len { + if i >= index { + return str[:totalSize] + } + + _, size := utf8.DecodeRuneInString(str[totalSize:]) + totalSize += size + i++ + } + + return str[:totalSize] +} + // SliceVisualEnd will take a byte slice and slice off the start // up to a given visual index. If the index is in the middle of a // rune the number of visual columns into the rune will be returned