Add infobar autocomplete

This commit is contained in:
Zachary Yedidia
2019-01-20 17:49:20 -05:00
parent ab37e6ad6c
commit fc7058d47c
7 changed files with 257 additions and 149 deletions

View File

@@ -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

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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++
}
}
}

View File

@@ -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
}

View File

@@ -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