From 123f8992b1b258bd649ea351ecb25ea9afc43dff Mon Sep 17 00:00:00 2001 From: TorchedSammy <38820196+TorchedSammy@users.noreply.github.com> Date: Mon, 22 Nov 2021 11:24:31 -0500 Subject: [PATCH] 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 --- go.mod | 1 + go.sum | 2 + lua.go | 16 +++++++ main.go | 1 + rl.go | 140 ++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 5 files changed, 157 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index fcfbb8d..1630c95 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.16 require ( 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/chuckpreslar/emission v0.0.0-20170206194824-a7ddd980baf9 github.com/mattn/go-runewidth v0.0.13 // indirect diff --git a/go.sum b/go.sum index 0b5c7e6..600840e 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ 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/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/go.mod h1:dh3avhLDhJJ/MJKzbak6FYn+DJKUWk7Fb6Dh5mGdv6Y= github.com/Rosettea/sh/v3 v3.4.0-0.dev.0.20211022004519-f67a49cb50f5 h1:ygwVRX8gf5MHA0VzSgOdscCEoAJLjM8joEotfQPgAd0= diff --git a/lua.go b/lua.go index 7d36ccd..0d90700 100644 --- a/lua.go +++ b/lua.go @@ -58,6 +58,8 @@ func LuaInit() { hooks = bait.New() l.PreloadModule("bait", hooks.Loader) + l.SetGlobal("complete", l.NewFunction(hshcomplete)) + // Add more paths that Lua can require from l.DoString("package.path = package.path .. " + requirePaths) @@ -210,3 +212,17 @@ func hshinterval(L *lua.LState) int { return 1 } +// complete(scope, cb) +// Registers a completion handler for `scope`. +// A `scope` is currently only expected to be `command.`, +// replacing 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 +} diff --git a/main.go b/main.go index ec66c47..9e21d2b 100644 --- a/main.go +++ b/main.go @@ -26,6 +26,7 @@ var ( commands = map[string]*lua.LFunction{} aliases = map[string]string{} + luaCompletions = map[string]*lua.LFunction{} homedir string confDir string diff --git a/rl.go b/rl.go index c1664e8..343cb02 100644 --- a/rl.go +++ b/rl.go @@ -7,15 +7,149 @@ package main // making them interchangable during build time // 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 { Prompt string } -// other gophers might hate this naming but this is local, shut up 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 == "" { + // 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) return &LineReader{