From 0ed365170c69d3e3fb65a993479e5717f58d473f Mon Sep 17 00:00:00 2001 From: TorchedSammy <38820196+TorchedSammy@users.noreply.github.com> Date: Sat, 5 Mar 2022 15:59:00 -0400 Subject: [PATCH] refactor!: completion api, add hilbish.completion interface this is a pretty big commit which mainly contains a refactor and breaking change to how command completions are done. before that, a hilbish.completion interface has been added which for now just has 2 functions (`files` and `bins`) for completions of normal files and executables. hilbish.complete is now expected to return a table of "completions groups," which are as the name suggests a group for a completion. a completion group is a table which has the fields `type`, which can be either `list` or `grid`, and `items`, being an array (or string keyed table) of items if an item is string keyed the item itself is the key name and the value is a table with the first value in it being the description for the item. this description is only applied with the list type. this is probably the longest commit message ive written --- api.go | 57 ++++++++++++++- complete.go | 53 +++++++++++++- exec.go | 2 +- rl.go | 203 +++++++++++++++++++++------------------------------- 4 files changed, 187 insertions(+), 128 deletions(-) diff --git a/api.go b/api.go index f2d4aa1..ed199a8 100644 --- a/api.go +++ b/api.go @@ -74,10 +74,11 @@ The nice lil shell for {blue}Lua{reset} fanatics! util.SetField(L, hshuser, "data", lua.LString(userDataDir), "XDG data directory") util.Document(L, hshuser, "User directories to store configs and/or modules.") L.SetField(mod, "userDir", hshuser) - + + // hilbish.os table hshos := L.NewTable() info, _ := osinfo.GetOSInfo() - + util.SetField(L, hshos, "family", lua.LString(info.Family), "Family name of the current OS") util.SetField(L, hshos, "name", lua.LString(info.Name), "Pretty name of the current OS") util.SetField(L, hshos, "version", lua.LString(info.Version), "Version of the current OS") @@ -95,11 +96,63 @@ The nice lil shell for {blue}Lua{reset} fanatics! util.Document(L, historyModule, "History interface for Hilbish.") L.SetField(mod, "history", historyModule) + // hilbish.completions table + hshcomp := L.NewTable() + + util.SetField(L, hshcomp, "files", L.NewFunction(luaFileComplete), "Completer for files") + util.SetField(L, hshcomp, "bins", L.NewFunction(luaBinaryComplete), "Completer for executables/binaries") + util.Document(L, hshcomp, "Completions interface for Hilbish.") + L.SetField(mod, "completion", hshcomp) + L.Push(mod) return 1 } +func luaFileComplete(L *lua.LState) int { + query := L.CheckString(1) + ctx := L.CheckString(2) + fields := L.CheckTable(3) + + var fds []string + fields.ForEach(func(k lua.LValue, v lua.LValue) { + fds = append(fds, v.String()) + }) + + completions := fileComplete(query, ctx, fds) + luaComps := L.NewTable() + + for _, comp := range completions { + luaComps.Append(lua.LString(comp)) + } + + L.Push(luaComps) + + return 1 +} + +func luaBinaryComplete(L *lua.LState) int { + query := L.CheckString(1) + ctx := L.CheckString(2) + fields := L.CheckTable(3) + + var fds []string + fields.ForEach(func(k lua.LValue, v lua.LValue) { + fds = append(fds, v.String()) + }) + + completions, _ := binaryComplete(query, ctx, fds) + luaComps := L.NewTable() + + for _, comp := range completions { + luaComps.Append(lua.LString(comp)) + } + + L.Push(luaComps) + + return 1 +} + func setVimMode(mode string) { hooks.Em.Emit("hilbish.vimMode", mode) util.SetField(l, hshMod, "vimMode", lua.LString(mode), "Current Vim mode of Hilbish (nil if not in Vim mode)") diff --git a/complete.go b/complete.go index c22fcc4..56009fb 100644 --- a/complete.go +++ b/complete.go @@ -15,7 +15,7 @@ func fileComplete(query, ctx string, fields []string) []string { completions, _ = matchPath(strings.Replace(query, "~", curuser.HomeDir, 1), query) } } - + if len(completions) == 0 && len(fields) > 1 { completions, _ = matchPath("./" + query, query) } @@ -23,6 +23,55 @@ func fileComplete(query, ctx string, fields []string) []string { return completions } +func binaryComplete(query, ctx string, fields []string) ([]string, string) { + var completions []string + + prefixes := []string{"./", "../", "/", "~/"} + for _, prefix := range prefixes { + if strings.HasPrefix(query, prefix) { + fileCompletions := fileComplete(query, ctx, fields) + if len(fileCompletions) != 0 { + for _, f := range fileCompletions { + name := strings.Replace(query + f, "~", curuser.HomeDir, 1) + if info, err := os.Stat(name); err == nil && info.Mode().Perm() & 0100 == 0 { + continue + } + completions = append(completions, f) + } + } + return completions, "" + } + } + + // filter out executables, but in path + for _, dir := range filepath.SplitList(os.Getenv("PATH")) { + // print dir to stderr for debugging + // search for an executable which matches our query string + if matches, err := filepath.Glob(filepath.Join(dir, query + "*")); err == nil { + // get basename from matches + for _, match := range matches { + // check if we have execute permissions for our match + if info, err := os.Stat(match); err == nil && info.Mode().Perm() & 0100 == 0 { + continue + } + // get basename from match + name := filepath.Base(match) + // add basename to completions + completions = append(completions, name) + } + } + } + + // add lua registered commands to completions + for cmdName := range commands { + if strings.HasPrefix(cmdName, query) { + completions = append(completions, cmdName) + } + } + + return completions, query +} + func matchPath(path, pref string) ([]string, error) { var entries []string matches, err := filepath.Glob(path + "*") @@ -54,6 +103,6 @@ func matchPath(path, pref string) ([]string, error) { entries = append(entries, name) } } - + return entries, err } diff --git a/exec.go b/exec.go index a482f6a..fc96878 100644 --- a/exec.go +++ b/exec.go @@ -111,7 +111,7 @@ func execCommand(cmd, old string) error { NRet: 1, Protect: true, }, luacmdArgs) - + if err != nil { fmt.Fprintln(os.Stderr, "Error in command:\n\n" + err.Error()) diff --git a/rl.go b/rl.go index d3dc6ff..9db7331 100644 --- a/rl.go +++ b/rl.go @@ -3,9 +3,7 @@ package main import ( "fmt" "io" - "path/filepath" "strings" - "os" "github.com/maxlandon/readline" "github.com/yuin/gopher-lua" @@ -40,13 +38,8 @@ func newLineReader(prompt string) *lineReader { rl.TabCompleter = func(line []rune, pos int, _ readline.DelayedTabContext) (string, []*readline.CompletionGroup) { ctx := string(line) var completions []string - - compGroup := []*readline.CompletionGroup{ - &readline.CompletionGroup{ - TrimSlash: false, - NoSpace: true, - }, - } + + var compGroup []*readline.CompletionGroup ctx = strings.TrimLeft(ctx, " ") if len(ctx) == 0 { @@ -60,62 +53,28 @@ func newLineReader(prompt string) *lineReader { query := fields[len(fields) - 1] ctx = aliases.Resolve(ctx) - - if len(fields) == 1 { - prefixes := []string{"./", "../", "/", "~/"} - for _, prefix := range prefixes { - if strings.HasPrefix(query, prefix) { - fileCompletions := fileComplete(query, ctx, fields) - if len(fileCompletions) != 0 { - for _, f := range fileCompletions { - name := strings.Replace(query + f, "~", curuser.HomeDir, 1) - if info, err := os.Stat(name); err == nil && info.Mode().Perm() & 0100 == 0 { - continue - } - completions = append(completions, f) - } - compGroup[0].Suggestions = completions - } - return "", compGroup - } - } - // filter out executables, but in path - for _, dir := range filepath.SplitList(os.Getenv("PATH")) { - // print dir to stderr for debugging - // search for an executable which matches our query string - if matches, err := filepath.Glob(filepath.Join(dir, query + "*")); err == nil { - // get basename from matches - for _, match := range matches { - // check if we have execute permissions for our match - if info, err := os.Stat(match); err == nil && info.Mode().Perm() & 0100 == 0 { - continue - } - // get basename from match - name := filepath.Base(match) - // print name to stderr for debugging - // add basename to completions - completions = append(completions, name) - } - } - } - - // add lua registered commands to completions - for cmdName := range commands { - if strings.HasPrefix(cmdName, query) { - completions = append(completions, cmdName) - } - } - - compGroup[0].Suggestions = completions - return query, compGroup + if len(fields) == 1 { + completions, prefix := binaryComplete(query, ctx, fields) + + compGroup = append(compGroup, &readline.CompletionGroup{ + TrimSlash: false, + NoSpace: true, + Suggestions: completions, + }) + + return prefix, compGroup } else { if completecb, ok := luaCompletions["command." + fields[0]]; ok { + luaFields := l.NewTable() + for _, f := range fields { + luaFields.Append(lua.LString(f)) + } err := l.CallByParam(lua.P{ Fn: completecb, NRet: 1, Protect: true, - }) + }, lua.LString(query), lua.LString(ctx), luaFields) if err != nil { return "", compGroup @@ -124,88 +83,86 @@ func newLineReader(prompt string) *lineReader { luacompleteTable := l.Get(-1) l.Pop(1) + /* + as an example with git, + completion table should be structured like: + { + { + items = { + 'add', + 'clone', + 'init' + }, + type = 'grid' + }, + { + items = { + '-c', + '--git-dir' + }, + type = 'list' + } + } + ^ a table of completion groups. + it is the responsibility of the completer + to work on subcommands and subcompletions + */ if cmpTbl, ok := luacompleteTable.(*lua.LTable); ok { cmpTbl.ForEach(func(key lua.LValue, value lua.LValue) { - // if key is a number (index), we just check and complete that if key.Type() == lua.LTNumber { - // if we have only 2 fields then this is fine - if len(fields) == 2 { - if strings.HasPrefix(value.String(), fields[1]) { - completions = append(completions, value.String()) + // completion group + if value.Type() == lua.LTTable { + luaCmpGroup := value.(*lua.LTable) + compType := luaCmpGroup.RawGet(lua.LString("type")) + compItems := luaCmpGroup.RawGet(lua.LString("items")) + if compType.Type() != lua.LTString { + l.RaiseError("bad type name for completion (expected string, got %v)", compType.Type().String()) } - } - } else if key.Type() == lua.LTString { - if len(fields) == 2 { - if strings.HasPrefix(key.String(), fields[1]) { - completions = append(completions, key.String()) + if compItems.Type() != lua.LTTable { + l.RaiseError("bad items for completion (expected table, got %v)", compItems.Type().String()) } - } else { - // if we have more than 2 fields, we need to check if the key matches - // the current field and if it does, we need to check if the value is a string - // or table (nested sub completions) - if key.String() == fields[1] { - // if value is a table, we need to iterate over it - // and add each value to completions - // check if value is either a table or function - if value.Type() == lua.LTTable { - valueTbl := value.(*lua.LTable) - valueTbl.ForEach(func(key lua.LValue, value lua.LValue) { - val := value.String() - if val == "" { - // complete files - completions = append(completions, fileComplete(query, ctx, fields)...) - } else { - if strings.HasPrefix(val, query) { - completions = append(completions, val) - } - } - }) - } else if value.Type() == lua.LTFunction { - // if value is a function, we need to call it - // and add each value to completions - // completionsCtx is the context we pass to the function, - // removing 2 fields from the fields array - completionsCtx := strings.Join(fields[2:], " ") - err := l.CallByParam(lua.P{ - Fn: value, - NRet: 1, - Protect: true, - }, lua.LString(query), lua.LString(completionsCtx)) - - if err != nil { - return - } - - luacompleteTable := l.Get(-1) - l.Pop(1) - - // just check if its actually a table and add it to the completions - if cmpTbl, ok := luacompleteTable.(*lua.LTable); ok { - cmpTbl.ForEach(func(key lua.LValue, value lua.LValue) { - val := value.String() - if strings.HasPrefix(val, query) { - completions = append(completions, val) - } - }) - } + var items []string + itemDescriptions := make(map[string]string) + compItems.(*lua.LTable).ForEach(func(k lua.LValue, v lua.LValue) { + if k.Type() == lua.LTString { + // ['--flag'] = {'description', '--flag-alias'} + itm := v.(*lua.LTable) + items = append(items, k.String()) + itemDescriptions[k.String()] = itm.RawGet(lua.LNumber(1)).String() } else { - // throw lua error - // complete.cmdname: error message... - l.RaiseError("complete." + fields[0] + ": completion value is not a table or function") + items = append(items, v.String()) } + }) + + var dispType readline.TabDisplayType + switch compType.String() { + case "grid": dispType = readline.TabDisplayGrid + case "list": dispType = readline.TabDisplayList + // need special cases, will implement later + //case "map": dispType = readline.TabDisplayMap } + compGroup = append(compGroup, &readline.CompletionGroup{ + DisplayType: dispType, + Descriptions: itemDescriptions, + Suggestions: items, + TrimSlash: false, + NoSpace: true, + }) } } }) } } - if len(completions) == 0 { + if len(compGroup) == 0 { completions = fileComplete(query, ctx, fields) + compGroup = append(compGroup, &readline.CompletionGroup{ + TrimSlash: false, + NoSpace: true, + Suggestions: completions, + }) } } - - compGroup[0].Suggestions = completions return "", compGroup }