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{