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
53
api.go
53
api.go
|
@ -75,6 +75,7 @@ The nice lil shell for {blue}Lua{reset} fanatics!
|
||||||
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()
|
||||||
|
|
||||||
|
@ -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)")
|
||||||
|
|
49
complete.go
49
complete.go
|
@ -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 + "*")
|
||||||
|
|
195
rl.go
195
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"
|
||||||
|
@ -41,12 +39,7 @@ func newLineReader(prompt string) *lineReader {
|
||||||
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 {
|
||||||
|
@ -62,60 +55,26 @@ func newLineReader(prompt string) *lineReader {
|
||||||
ctx = aliases.Resolve(ctx)
|
ctx = aliases.Resolve(ctx)
|
||||||
|
|
||||||
if len(fields) == 1 {
|
if len(fields) == 1 {
|
||||||
prefixes := []string{"./", "../", "/", "~/"}
|
completions, prefix := binaryComplete(query, ctx, fields)
|
||||||
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
|
compGroup = append(compGroup, &readline.CompletionGroup{
|
||||||
for _, dir := range filepath.SplitList(os.Getenv("PATH")) {
|
TrimSlash: false,
|
||||||
// print dir to stderr for debugging
|
NoSpace: true,
|
||||||
// search for an executable which matches our query string
|
Suggestions: completions,
|
||||||
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)
|
|
||||||
// print name to stderr for debugging
|
|
||||||
// add basename to completions
|
|
||||||
completions = append(completions, name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// add lua registered commands to completions
|
return prefix, compGroup
|
||||||
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