mirror of https://github.com/Hilbis/Hilbish
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 writtenwindows-fixes
parent
70724ec015
commit
0ed365170c
57
api.go
57
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.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)")
|
||||||
|
|
53
complete.go
53
complete.go
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
2
exec.go
2
exec.go
|
@ -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
203
rl.go
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue