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
windows-fixes
TorchedSammy 2022-03-05 15:59:00 -04:00
parent 70724ec015
commit 0ed365170c
Signed by: sammyette
GPG Key ID: 904FC49417B44DCD
4 changed files with 187 additions and 128 deletions

57
api.go
View File

@ -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.SetField(L, hshuser, "data", lua.LString(userDataDir), "XDG data directory")
util.Document(L, hshuser, "User directories to store configs and/or modules.") util.Document(L, hshuser, "User directories to store configs and/or modules.")
L.SetField(mod, "userDir", hshuser) L.SetField(mod, "userDir", hshuser)
// hilbish.os table
hshos := L.NewTable() hshos := L.NewTable()
info, _ := osinfo.GetOSInfo() info, _ := osinfo.GetOSInfo()
util.SetField(L, hshos, "family", lua.LString(info.Family), "Family name of the current OS") 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, "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") 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.") util.Document(L, historyModule, "History interface for Hilbish.")
L.SetField(mod, "history", historyModule) 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) L.Push(mod)
return 1 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) { func setVimMode(mode string) {
hooks.Em.Emit("hilbish.vimMode", mode) 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)") util.SetField(l, hshMod, "vimMode", lua.LString(mode), "Current Vim mode of Hilbish (nil if not in Vim mode)")

View File

@ -15,7 +15,7 @@ func fileComplete(query, ctx string, fields []string) []string {
completions, _ = matchPath(strings.Replace(query, "~", curuser.HomeDir, 1), query) completions, _ = matchPath(strings.Replace(query, "~", curuser.HomeDir, 1), query)
} }
} }
if len(completions) == 0 && len(fields) > 1 { if len(completions) == 0 && len(fields) > 1 {
completions, _ = matchPath("./" + query, query) completions, _ = matchPath("./" + query, query)
} }
@ -23,6 +23,55 @@ func fileComplete(query, ctx string, fields []string) []string {
return completions 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) { func matchPath(path, pref string) ([]string, error) {
var entries []string var entries []string
matches, err := filepath.Glob(path + "*") matches, err := filepath.Glob(path + "*")
@ -54,6 +103,6 @@ func matchPath(path, pref string) ([]string, error) {
entries = append(entries, name) entries = append(entries, name)
} }
} }
return entries, err return entries, err
} }

View File

@ -111,7 +111,7 @@ func execCommand(cmd, old string) error {
NRet: 1, NRet: 1,
Protect: true, Protect: true,
}, luacmdArgs) }, luacmdArgs)
if err != nil { if err != nil {
fmt.Fprintln(os.Stderr, fmt.Fprintln(os.Stderr,
"Error in command:\n\n" + err.Error()) "Error in command:\n\n" + err.Error())

203
rl.go
View File

@ -3,9 +3,7 @@ package main
import ( import (
"fmt" "fmt"
"io" "io"
"path/filepath"
"strings" "strings"
"os"
"github.com/maxlandon/readline" "github.com/maxlandon/readline"
"github.com/yuin/gopher-lua" "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) { rl.TabCompleter = func(line []rune, pos int, _ readline.DelayedTabContext) (string, []*readline.CompletionGroup) {
ctx := string(line) ctx := string(line)
var completions []string var completions []string
compGroup := []*readline.CompletionGroup{ var compGroup []*readline.CompletionGroup
&readline.CompletionGroup{
TrimSlash: false,
NoSpace: true,
},
}
ctx = strings.TrimLeft(ctx, " ") ctx = strings.TrimLeft(ctx, " ")
if len(ctx) == 0 { if len(ctx) == 0 {
@ -60,62 +53,28 @@ func newLineReader(prompt string) *lineReader {
query := fields[len(fields) - 1] query := fields[len(fields) - 1]
ctx = aliases.Resolve(ctx) 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 if len(fields) == 1 {
for _, dir := range filepath.SplitList(os.Getenv("PATH")) { completions, prefix := binaryComplete(query, ctx, fields)
// print dir to stderr for debugging
// search for an executable which matches our query string compGroup = append(compGroup, &readline.CompletionGroup{
if matches, err := filepath.Glob(filepath.Join(dir, query + "*")); err == nil { TrimSlash: false,
// get basename from matches NoSpace: true,
for _, match := range matches { Suggestions: completions,
// check if we have execute permissions for our match })
if info, err := os.Stat(match); err == nil && info.Mode().Perm() & 0100 == 0 {
continue return prefix, compGroup
}
// 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
} else { } else {
if completecb, ok := luaCompletions["command." + fields[0]]; ok { 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{ err := l.CallByParam(lua.P{
Fn: completecb, Fn: completecb,
NRet: 1, NRet: 1,
Protect: true, Protect: true,
}) }, lua.LString(query), lua.LString(ctx), luaFields)
if err != nil { if err != nil {
return "", compGroup return "", compGroup
@ -124,88 +83,86 @@ func newLineReader(prompt string) *lineReader {
luacompleteTable := l.Get(-1) luacompleteTable := l.Get(-1)
l.Pop(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 { if cmpTbl, ok := luacompleteTable.(*lua.LTable); ok {
cmpTbl.ForEach(func(key lua.LValue, value lua.LValue) { 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 key.Type() == lua.LTNumber {
// if we have only 2 fields then this is fine // completion group
if len(fields) == 2 { if value.Type() == lua.LTTable {
if strings.HasPrefix(value.String(), fields[1]) { luaCmpGroup := value.(*lua.LTable)
completions = append(completions, value.String()) 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())
} }
} if compItems.Type() != lua.LTTable {
} else if key.Type() == lua.LTString { l.RaiseError("bad items for completion (expected table, got %v)", compItems.Type().String())
if len(fields) == 2 {
if strings.HasPrefix(key.String(), fields[1]) {
completions = append(completions, key.String())
} }
} else { var items []string
// if we have more than 2 fields, we need to check if the key matches itemDescriptions := make(map[string]string)
// the current field and if it does, we need to check if the value is a string compItems.(*lua.LTable).ForEach(func(k lua.LValue, v lua.LValue) {
// or table (nested sub completions) if k.Type() == lua.LTString {
if key.String() == fields[1] { // ['--flag'] = {'description', '--flag-alias'}
// if value is a table, we need to iterate over it itm := v.(*lua.LTable)
// and add each value to completions items = append(items, k.String())
// check if value is either a table or function itemDescriptions[k.String()] = itm.RawGet(lua.LNumber(1)).String()
if value.Type() == lua.LTTable {
valueTbl := value.(*lua.LTable)
valueTbl.ForEach(func(key lua.LValue, value lua.LValue) {
val := value.String()
if val == "<file>" {
// 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)
}
})
}
} else { } else {
// throw lua error items = append(items, v.String())
// complete.cmdname: error message...
l.RaiseError("complete." + fields[0] + ": completion value is not a table or function")
} }
})
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) completions = fileComplete(query, ctx, fields)
compGroup = append(compGroup, &readline.CompletionGroup{
TrimSlash: false,
NoSpace: true,
Suggestions: completions,
})
} }
} }
compGroup[0].Suggestions = completions
return "", compGroup return "", compGroup
} }