feat: add tab completion api

tab complete is better than it was before!
there is a new `complete` function which allows adding custom arguments
to complete specific functions.
hilbish will now also complete executables if it's the first input
argument (this also works with ./)
if no completion is added for a command, hilbish will just complete
files
dev
TorchedSammy 2021-11-22 11:24:31 -05:00
parent 77cc7fe24a
commit 123f8992b1
Signed by: sammyette
GPG Key ID: 904FC49417B44DCD
5 changed files with 157 additions and 3 deletions

1
go.mod
View File

@ -4,6 +4,7 @@ go 1.16
require ( require (
github.com/Rosettea/Hilbiline v0.0.0-20210603231612-80054dac3650 github.com/Rosettea/Hilbiline v0.0.0-20210603231612-80054dac3650
github.com/Rosettea/readline v0.0.0-20211122152601-6d95ce44b7ed
github.com/bobappleyard/readline v0.0.0-20150707195538-7e300e02d38e github.com/bobappleyard/readline v0.0.0-20150707195538-7e300e02d38e
github.com/chuckpreslar/emission v0.0.0-20170206194824-a7ddd980baf9 github.com/chuckpreslar/emission v0.0.0-20170206194824-a7ddd980baf9
github.com/mattn/go-runewidth v0.0.13 // indirect github.com/mattn/go-runewidth v0.0.13 // indirect

2
go.sum
View File

@ -1,5 +1,7 @@
github.com/Rosettea/Hilbiline v0.0.0-20210603231612-80054dac3650 h1:nzFJUdJU8UJ1DA8mSQp4eoBtQyOJyecekVWusjfQsqE= github.com/Rosettea/Hilbiline v0.0.0-20210603231612-80054dac3650 h1:nzFJUdJU8UJ1DA8mSQp4eoBtQyOJyecekVWusjfQsqE=
github.com/Rosettea/Hilbiline v0.0.0-20210603231612-80054dac3650/go.mod h1:/FFZ4cgR6TXXYaskRUxyLIYdfG0PS4BPtWjWRQms754= github.com/Rosettea/Hilbiline v0.0.0-20210603231612-80054dac3650/go.mod h1:/FFZ4cgR6TXXYaskRUxyLIYdfG0PS4BPtWjWRQms754=
github.com/Rosettea/readline v0.0.0-20211122152601-6d95ce44b7ed h1:sGsGPG+b5h9OR1GjM0PiM4iemB9hmi0o8cg2YRSRKko=
github.com/Rosettea/readline v0.0.0-20211122152601-6d95ce44b7ed/go.mod h1:OH+WJSCks0t2ISvaCFUT4ZxNGr4Etq4ju9JE/UxH/5o=
github.com/Rosettea/sh/v3 v3.3.0 h1:0/xmOfzpy46gB1I2oPj8QwdYvyJzpdF5STcgNPRQHcI= github.com/Rosettea/sh/v3 v3.3.0 h1:0/xmOfzpy46gB1I2oPj8QwdYvyJzpdF5STcgNPRQHcI=
github.com/Rosettea/sh/v3 v3.3.0/go.mod h1:dh3avhLDhJJ/MJKzbak6FYn+DJKUWk7Fb6Dh5mGdv6Y= github.com/Rosettea/sh/v3 v3.3.0/go.mod h1:dh3avhLDhJJ/MJKzbak6FYn+DJKUWk7Fb6Dh5mGdv6Y=
github.com/Rosettea/sh/v3 v3.4.0-0.dev.0.20211022004519-f67a49cb50f5 h1:ygwVRX8gf5MHA0VzSgOdscCEoAJLjM8joEotfQPgAd0= github.com/Rosettea/sh/v3 v3.4.0-0.dev.0.20211022004519-f67a49cb50f5 h1:ygwVRX8gf5MHA0VzSgOdscCEoAJLjM8joEotfQPgAd0=

16
lua.go
View File

@ -58,6 +58,8 @@ func LuaInit() {
hooks = bait.New() hooks = bait.New()
l.PreloadModule("bait", hooks.Loader) l.PreloadModule("bait", hooks.Loader)
l.SetGlobal("complete", l.NewFunction(hshcomplete))
// Add more paths that Lua can require from // Add more paths that Lua can require from
l.DoString("package.path = package.path .. " + requirePaths) l.DoString("package.path = package.path .. " + requirePaths)
@ -210,3 +212,17 @@ func hshinterval(L *lua.LState) int {
return 1 return 1
} }
// complete(scope, cb)
// Registers a completion handler for `scope`.
// A `scope` is currently only expected to be `command.<cmd>`,
// replacing <cmd> with the name of the command (for example `command.git`).
// `cb` must be a function that returns a table of the entries to complete.
// Nested tables will be used as sub-completions.
func hshcomplete(L *lua.LState) int {
scope := L.CheckString(1)
cb := L.CheckFunction(2)
luaCompletions[scope] = cb
return 0
}

View File

@ -26,6 +26,7 @@ var (
commands = map[string]*lua.LFunction{} commands = map[string]*lua.LFunction{}
aliases = map[string]string{} aliases = map[string]string{}
luaCompletions = map[string]*lua.LFunction{}
homedir string homedir string
confDir string confDir string

140
rl.go
View File

@ -7,15 +7,149 @@ package main
// making them interchangable during build time // making them interchangable during build time
// this is normal readline // this is normal readline
import "github.com/bobappleyard/readline" import (
"path/filepath"
"strings"
"os"
"github.com/Rosettea/readline"
"github.com/yuin/gopher-lua"
)
type LineReader struct { type LineReader struct {
Prompt string Prompt string
} }
// other gophers might hate this naming but this is local, shut up
func NewLineReader(prompt string) *LineReader { func NewLineReader(prompt string) *LineReader {
readline.Completer = readline.FilenameCompleter readline.Completer = func(query string, ctx string) []string {
var completions []string
// trim whitespace from ctx
ctx = strings.TrimLeft(ctx, " ")
fields := strings.Split(ctx, " ")
if len(fields) == 0 {
return nil
}
if len(fields) == 1 {
prefixes := []string{"./", "../"}
for _, prefix := range prefixes {
if strings.HasPrefix(query, prefix) {
if matches, err := filepath.Glob(query + "*"); err == nil {
for _, match := range matches {
if info, err := os.Stat(match); err == nil && info.Mode().Perm() & 0100 == 0 {
continue
}
name := filepath.Base(match)
completions = append(completions, name)
}
}
if len(completions) == 1 {
// we have add the base dir of query since the completion entries are basename
// why? so readline will display just that
// and we want to complete the full path when its the only completion entry
// query will be incomplete so adding it will be broken
// also Dir doesn't have a trailing slash so we need to filepath join
// to account to windows
// AND ANOTHER THING is it returns . if the arg is ./ and Join will
// ignore that so we have to check and just add the prefix instead
if prefix != "./" {
completions[0] = filepath.Join(filepath.Dir(query), completions[0])
} else {
completions[0] = prefix + completions[0]
}
}
return completions
}
}
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)
// 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)
}
}
} else {
if completecb, ok := luaCompletions["command." + fields[0]]; ok {
err := l.CallByParam(lua.P{
Fn: completecb,
NRet: 1,
Protect: true,
})
if err != nil {
return []string{}
}
luacompleteTable := l.Get(-1)
l.Pop(1)
if cmpTbl, ok := luacompleteTable.(*lua.LTable); ok {
cmpTbl.ForEach(func(key lua.LValue, value lua.LValue) {
// print key and value to stderr for debugging
// if key is a number (index), we just check and complete that
if key.Type() == lua.LTNumber {
// if we have only 2 fields then this is fine
if len(fields) == 2 {
if strings.HasPrefix(value.String(), fields[1]) {
completions = append(completions, value.String())
}
}
} else if key.Type() == lua.LTString {
if len(fields) == 2 {
if strings.HasPrefix(key.String(), fields[1]) {
completions = append(completions, key.String())
}
} else {
// if we have more than 2 fields, we need to check if the key matches
// the current field and if it does, we need to check if the value is a string
// or table (nested sub completions)
if key.String() == fields[1] {
// if value is a table, we need to iterate over it
// and add each value to completions
valueTbl := value.(*lua.LTable)
valueTbl.ForEach(func(key lua.LValue, value lua.LValue) {
val := value.String()
if val == "<file>" {
// complete files
completions = append(completions, readline.FilenameCompleter(query, ctx)...)
} else {
if strings.HasPrefix(val, query) {
completions = append(completions, val)
}
}
})
}
}
}
})
}
}
if len(completions) == 0 {
completions = readline.FilenameCompleter(query, ctx)
}
}
return completions
}
readline.LoadHistory(defaultHistPath) readline.LoadHistory(defaultHistPath)
return &LineReader{ return &LineReader{