From abfbeb5f84d1c97f08c399b226c5191c22aed12f Mon Sep 17 00:00:00 2001 From: TorchedSammy <38820196+TorchedSammy@users.noreply.github.com> Date: Fri, 22 Apr 2022 21:16:35 -0400 Subject: [PATCH] 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. --- api.go | 14 +--- complete.go | 104 +++++++++++++++++++++++++- rl.go | 206 +++++++++++++++++++--------------------------------- 3 files changed, 174 insertions(+), 150 deletions(-) diff --git a/api.go b/api.go index 94263a2..d454b8e 100644 --- a/api.go +++ b/api.go @@ -149,19 +149,7 @@ Check out the {blue}{bold}guide{reset} command to get started. util.Document(historyModule, "History interface for Hilbish.") // hilbish.completion table - hshcomp := rt.NewTable() - 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") - + hshcomp := completionLoader(rtm) util.Document(hshcomp, "Completions interface for Hilbish.") mod.Set(rt.StringValue("completion"), rt.TableValue(hshcomp)) diff --git a/complete.go b/complete.go index 68f5d67..1e36681 100644 --- a/complete.go +++ b/complete.go @@ -11,6 +11,8 @@ import ( rt "github.com/arnodel/golua/runtime" ) +var completer rt.Value + func fileComplete(query, ctx string, fields []string) ([]string, string) { return matchPath(query) } @@ -117,6 +119,100 @@ func escapeFilename(fname string) string { 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) { if err := c.CheckNArgs(4); err != nil { return nil, err @@ -162,14 +258,14 @@ func luaFileComplete(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { return nil, err } - completions, _ := fileComplete(query, ctx, fds) + completions, pfx := fileComplete(query, ctx, fds) luaComps := rt.NewTable() for i, comp := range completions { 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) { @@ -178,14 +274,14 @@ func luaBinaryComplete(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { return nil, err } - completions, _ := binaryComplete(query, ctx, fds) + completions, pfx := binaryComplete(query, ctx, fds) luaComps := rt.NewTable() for i, comp := range completions { 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) { diff --git a/rl.go b/rl.go index cd1f4eb..88adc65 100644 --- a/rl.go +++ b/rl.go @@ -84,151 +84,91 @@ func newLineReader(prompt string, noHist bool) *lineReader { return highlighted } 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 - - ctx = strings.TrimLeft(ctx, " ") - if len(ctx) == 0 { - return "", compGroup + var compGroups []*readline.CompletionGroup + if err != nil { + return "", compGroups } - fields := strings.Split(ctx, " ") - if len(fields) == 0 { - return "", compGroup + luaCompGroups := term.Get(0) + luaPrefix := term.Get(1) + + 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 { - completions, prefix := binaryComplete(query, ctx, fields) + util.ForEach(groups, func(key rt.Value, val rt.Value) { + if key.Type() != rt.IntType || val.Type() != rt.TableType { + return + } - compGroup = append(compGroup, &readline.CompletionGroup{ - TrimSlash: false, - NoSpace: true, - Suggestions: completions, + 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 + } }) - return prefix, compGroup - } else { - if completecb, ok := luaCompletions["command." + fields[0]]; ok { - luaFields := rt.NewTable() - for i, f := range fields { - luaFields.Set(rt.IntValue(int64(i + 1)), rt.StringValue(f)) - } - - // 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, - }) - }) - } + 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 } - if len(compGroup) == 0 { - completions, p := fileComplete(query, ctx, fields) - fcompGroup := []*readline.CompletionGroup{{ - TrimSlash: false, - NoSpace: true, - Suggestions: completions, - }} + compGroups = append(compGroups, &readline.CompletionGroup{ + DisplayType: dispType, + Descriptions: itemDescriptions, + Suggestions: items, + TrimSlash: false, + NoSpace: true, + }) + }) - return p, fcompGroup - } - } - return "", compGroup + return pfx, compGroups } return &lineReader{