feat: allow overwrite of completion handler (closes #122)

this also makes the completion functions `bins`
and `files` also return the prefix to pass
to the completion handler.

this is an overhaul to the completion system,
which gets the completion handler from lua
instead of being made to only have lua provided
*command* completions.

it does not have any performance deficit, even
though it calls in to golua for completions.
insensitive-tab^2
TorchedSammy 2022-04-22 21:16:35 -04:00
parent 3194add3dc
commit abfbeb5f84
Signed by: sammyette
GPG Key ID: 904FC49417B44DCD
3 changed files with 174 additions and 150 deletions

14
api.go
View File

@ -149,19 +149,7 @@ Check out the {blue}{bold}guide{reset} command to get started.
util.Document(historyModule, "History interface for Hilbish.") util.Document(historyModule, "History interface for Hilbish.")
// hilbish.completion table // hilbish.completion table
hshcomp := rt.NewTable() hshcomp := completionLoader(rtm)
util.SetField(rtm, hshcomp, "files",
rt.FunctionValue(rt.NewGoFunction(luaFileComplete, "files", 3, false)),
"Completer for files")
util.SetField(rtm, hshcomp, "bins",
rt.FunctionValue(rt.NewGoFunction(luaBinaryComplete, "bins", 3, false)),
"Completer for executables/binaries")
util.SetField(rtm, hshcomp, "call",
rt.FunctionValue(rt.NewGoFunction(callLuaCompleter, "call", 4, false)),
"Calls a completer and get its entries for completions")
util.Document(hshcomp, "Completions interface for Hilbish.") util.Document(hshcomp, "Completions interface for Hilbish.")
mod.Set(rt.StringValue("completion"), rt.TableValue(hshcomp)) mod.Set(rt.StringValue("completion"), rt.TableValue(hshcomp))

View File

@ -11,6 +11,8 @@ import (
rt "github.com/arnodel/golua/runtime" rt "github.com/arnodel/golua/runtime"
) )
var completer rt.Value
func fileComplete(query, ctx string, fields []string) ([]string, string) { func fileComplete(query, ctx string, fields []string) ([]string, string) {
return matchPath(query) return matchPath(query)
} }
@ -117,6 +119,100 @@ func escapeFilename(fname string) string {
return r.Replace(fname) return r.Replace(fname)
} }
func completionLoader(rtm *rt.Runtime) *rt.Table {
exports := map[string]util.LuaExport{
"files": {luaFileComplete, 3, false},
"bins": {luaBinaryComplete, 3, false},
"call": {callLuaCompleter, 4, false},
"handler": {completionHandler, 2, false},
}
mod := rt.NewTable()
util.SetExports(rtm, mod, exports)
return mod
}
func completionHandler(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err := c.CheckNArgs(2); err != nil {
return nil, err
}
line, err := c.StringArg(0)
if err != nil {
return nil, err
}
// just for validation
_, err = c.IntArg(1)
if err != nil {
return nil, err
}
ctx := strings.TrimLeft(line, " ")
if len(ctx) == 0 {
return c.PushingNext(t.Runtime, rt.TableValue(rt.NewTable()), rt.StringValue("")), nil
}
ctx = aliases.Resolve(ctx)
fields := strings.Split(ctx, " ")
query := fields[len(fields) - 1]
luaFields := rt.NewTable()
for i, f := range fields {
luaFields.Set(rt.IntValue(int64(i + 1)), rt.StringValue(f))
}
compMod := hshMod.Get(rt.StringValue("completion")).AsTable()
var term *rt.Termination
if len(fields) == 1 {
term = rt.NewTerminationWith(t.CurrentCont(), 2, false)
err := rt.Call(t, compMod.Get(rt.StringValue("bins")), []rt.Value{
rt.StringValue(query),
rt.StringValue(ctx),
rt.TableValue(luaFields),
}, term)
if err != nil {
return nil, err
}
} else {
gterm := rt.NewTerminationWith(t.CurrentCont(), 2, false)
err := rt.Call(t, compMod.Get(rt.StringValue("call")), []rt.Value{
rt.StringValue("commands." + fields[0]),
rt.StringValue(query),
rt.StringValue(ctx),
rt.TableValue(luaFields),
}, gterm)
if err == nil {
groups := gterm.Get(0)
pfx := gterm.Get(1)
return c.PushingNext(t.Runtime, groups, pfx), nil
}
// error means there isnt a command handler - default to files in that case
term = rt.NewTerminationWith(t.CurrentCont(), 2, false)
err = rt.Call(t, compMod.Get(rt.StringValue("files")), []rt.Value{
rt.StringValue(query),
rt.StringValue(ctx),
rt.TableValue(luaFields),
}, term)
}
comps := term.Get(0)
pfx := term.Get(1)
groups := rt.NewTable()
compGroup := rt.NewTable()
compGroup.Set(rt.StringValue("items"), comps)
compGroup.Set(rt.StringValue("type"), rt.StringValue("grid"))
groups.Set(rt.IntValue(1), rt.TableValue(compGroup))
return c.PushingNext(t.Runtime, rt.TableValue(groups), pfx), nil
}
func callLuaCompleter(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { func callLuaCompleter(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err := c.CheckNArgs(4); err != nil { if err := c.CheckNArgs(4); err != nil {
return nil, err return nil, err
@ -162,14 +258,14 @@ func luaFileComplete(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
return nil, err return nil, err
} }
completions, _ := fileComplete(query, ctx, fds) completions, pfx := fileComplete(query, ctx, fds)
luaComps := rt.NewTable() luaComps := rt.NewTable()
for i, comp := range completions { for i, comp := range completions {
luaComps.Set(rt.IntValue(int64(i + 1)), rt.StringValue(comp)) luaComps.Set(rt.IntValue(int64(i + 1)), rt.StringValue(comp))
} }
return c.PushingNext1(t.Runtime, rt.TableValue(luaComps)), nil return c.PushingNext(t.Runtime, rt.TableValue(luaComps), rt.StringValue(pfx)), nil
} }
func luaBinaryComplete(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { func luaBinaryComplete(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
@ -178,14 +274,14 @@ func luaBinaryComplete(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
return nil, err return nil, err
} }
completions, _ := binaryComplete(query, ctx, fds) completions, pfx := binaryComplete(query, ctx, fds)
luaComps := rt.NewTable() luaComps := rt.NewTable()
for i, comp := range completions { for i, comp := range completions {
luaComps.Set(rt.IntValue(int64(i + 1)), rt.StringValue(comp)) luaComps.Set(rt.IntValue(int64(i + 1)), rt.StringValue(comp))
} }
return c.PushingNext1(t.Runtime, rt.TableValue(luaComps)), nil return c.PushingNext(t.Runtime, rt.TableValue(luaComps), rt.StringValue(pfx)), nil
} }
func getCompleteParams(t *rt.Thread, c *rt.GoCont) (string, string, []string, error) { func getCompleteParams(t *rt.Thread, c *rt.GoCont) (string, string, []string, error) {

206
rl.go
View File

@ -84,151 +84,91 @@ func newLineReader(prompt string, noHist bool) *lineReader {
return highlighted return highlighted
} }
rl.TabCompleter = func(line []rune, pos int, _ readline.DelayedTabContext) (string, []*readline.CompletionGroup) { rl.TabCompleter = func(line []rune, pos int, _ readline.DelayedTabContext) (string, []*readline.CompletionGroup) {
ctx := string(line) term := rt.NewTerminationWith(l.MainThread().CurrentCont(), 2, false)
compHandle := hshMod.Get(rt.StringValue("completion")).AsTable().Get(rt.StringValue("handler"))
err := rt.Call(l.MainThread(), compHandle, []rt.Value{rt.StringValue(string(line)),
rt.IntValue(int64(pos))}, term)
var compGroup []*readline.CompletionGroup var compGroups []*readline.CompletionGroup
if err != nil {
ctx = strings.TrimLeft(ctx, " ") return "", compGroups
if len(ctx) == 0 {
return "", compGroup
} }
fields := strings.Split(ctx, " ") luaCompGroups := term.Get(0)
if len(fields) == 0 { luaPrefix := term.Get(1)
return "", compGroup
if luaCompGroups.Type() != rt.TableType {
return "", compGroups
} }
query := fields[len(fields) - 1]
ctx = aliases.Resolve(ctx) groups := luaCompGroups.AsTable()
// prefix is optional
pfx, _ := luaPrefix.TryString()
if len(fields) == 1 { util.ForEach(groups, func(key rt.Value, val rt.Value) {
completions, prefix := binaryComplete(query, ctx, fields) if key.Type() != rt.IntType || val.Type() != rt.TableType {
return
}
compGroup = append(compGroup, &readline.CompletionGroup{ valTbl := val.AsTable()
TrimSlash: false, luaCompType := valTbl.Get(rt.StringValue("type"))
NoSpace: true, luaCompItems := valTbl.Get(rt.StringValue("items"))
Suggestions: completions,
if luaCompType.Type() != rt.StringType || luaCompItems.Type() != rt.TableType {
return
}
items := []string{}
itemDescriptions := make(map[string]string)
util.ForEach(luaCompItems.AsTable(), func(lkey rt.Value, lval rt.Value) {
if keytyp := lkey.Type(); keytyp == rt.StringType {
// ['--flag'] = {'description', '--flag-alias'}
itemName, ok := lkey.TryString()
vlTbl, okk := lval.TryTable()
if !ok && !okk {
// TODO: error
return
}
items = append(items, itemName)
itemDescription, ok := vlTbl.Get(rt.IntValue(1)).TryString()
if !ok {
// TODO: error
return
}
itemDescriptions[itemName] = itemDescription
} else if keytyp == rt.IntType {
vlStr, ok := lval.TryString()
if !ok {
// TODO: error
return
}
items = append(items, vlStr)
} else {
// TODO: error
return
}
}) })
return prefix, compGroup var dispType readline.TabDisplayType
} else { switch luaCompType.AsString() {
if completecb, ok := luaCompletions["command." + fields[0]]; ok { case "grid": dispType = readline.TabDisplayGrid
luaFields := rt.NewTable() case "list": dispType = readline.TabDisplayList
for i, f := range fields { // need special cases, will implement later
luaFields.Set(rt.IntValue(int64(i + 1)), rt.StringValue(f)) //case "map": dispType = readline.TabDisplayMap
}
// we must keep the holy 80 cols
luacompleteTable, err := rt.Call1(l.MainThread(),
rt.FunctionValue(completecb), rt.StringValue(query),
rt.StringValue(ctx), rt.TableValue(luaFields))
if err != nil {
return "", compGroup
}
/*
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.TryTable(); ok {
util.ForEach(cmpTbl, func(key rt.Value, val rt.Value) {
if key.Type() != rt.IntType && val.Type() != rt.TableType {
return
}
valTbl := val.AsTable()
luaCompType := valTbl.Get(rt.StringValue("type"))
luaCompItems := valTbl.Get(rt.StringValue("items"))
if luaCompType.Type() != rt.StringType && luaCompItems.Type() != rt.TableType {
return
}
items := []string{}
itemDescriptions := make(map[string]string)
util.ForEach(luaCompItems.AsTable(), func(lkey rt.Value, lval rt.Value) {
if keytyp := lkey.Type(); keytyp == rt.StringType {
// ['--flag'] = {'description', '--flag-alias'}
itemName, ok := lkey.TryString()
vlTbl, okk := lval.TryTable()
if !ok && !okk {
// TODO: error
return
}
items = append(items, itemName)
itemDescription, ok := vlTbl.Get(rt.IntValue(1)).TryString()
if !ok {
// TODO: error
return
}
itemDescriptions[itemName] = itemDescription
} else if keytyp == rt.IntType {
vlStr, ok := lval.TryString()
if !ok {
// TODO: error
return
}
items = append(items, vlStr)
} else {
// TODO: error
return
}
})
var dispType readline.TabDisplayType
switch luaCompType.AsString() {
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(compGroup) == 0 { compGroups = append(compGroups, &readline.CompletionGroup{
completions, p := fileComplete(query, ctx, fields) DisplayType: dispType,
fcompGroup := []*readline.CompletionGroup{{ Descriptions: itemDescriptions,
TrimSlash: false, Suggestions: items,
NoSpace: true, TrimSlash: false,
Suggestions: completions, NoSpace: true,
}} })
})
return p, fcompGroup return pfx, compGroups
}
}
return "", compGroup
} }
return &lineReader{ return &lineReader{