-
-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #11 from tucksaun/feat/autocomplete
Implement generic autocompletion
- Loading branch information
Showing
18 changed files
with
834 additions
and
134 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
//go:build darwin || linux || freebsd || openbsd | ||
|
||
package console | ||
|
||
import ( | ||
"fmt" | ||
"os" | ||
"runtime/debug" | ||
|
||
"github.com/posener/complete/v2" | ||
) | ||
|
||
func init() { | ||
for _, key := range []string{"COMP_LINE", "COMP_POINT", "COMP_DEBUG"} { | ||
if _, hasEnv := os.LookupEnv(key); hasEnv { | ||
// Disable Garbage collection for faster autocompletion | ||
debug.SetGCPercent(-1) | ||
return | ||
} | ||
} | ||
} | ||
|
||
var autoCompleteCommand = &Command{ | ||
Category: "self", | ||
Name: "autocomplete", | ||
Description: "Internal command to provide shell completion suggestions", | ||
Hidden: Hide, | ||
FlagParsing: FlagParsingSkippedAfterFirstArg, | ||
Args: ArgDefinition{ | ||
&Arg{ | ||
Slice: true, | ||
Optional: true, | ||
}, | ||
}, | ||
Action: AutocompleteAppAction, | ||
} | ||
|
||
func registerAutocompleteCommands(a *Application) { | ||
if IsGoRun() { | ||
return | ||
} | ||
|
||
a.Commands = append( | ||
[]*Command{shellAutoCompleteInstallCommand, autoCompleteCommand}, | ||
a.Commands..., | ||
) | ||
} | ||
|
||
func AutocompleteAppAction(c *Context) error { | ||
cmd := complete.Command{ | ||
Flags: map[string]complete.Predictor{}, | ||
Sub: map[string]*complete.Command{}, | ||
} | ||
|
||
// transpose registered commands and flags to posener/complete equivalence | ||
for _, command := range c.App.VisibleCommands() { | ||
subCmd := command.convertToPosenerCompleteCommand(c) | ||
|
||
for _, name := range command.Names() { | ||
cmd.Sub[name] = &subCmd | ||
} | ||
} | ||
|
||
for _, f := range c.App.VisibleFlags() { | ||
if vf, ok := f.(*verbosityFlag); ok { | ||
vf.addToPosenerFlags(c, cmd.Flags) | ||
continue | ||
} | ||
|
||
predictor := ContextPredictor{f, c} | ||
|
||
for _, name := range f.Names() { | ||
name = fmt.Sprintf("%s%s", prefixFor(name), name) | ||
cmd.Flags[name] = predictor | ||
} | ||
} | ||
|
||
cmd.Complete(c.App.HelpName) | ||
return nil | ||
} | ||
|
||
func (c *Command) convertToPosenerCompleteCommand(ctx *Context) complete.Command { | ||
command := complete.Command{ | ||
Flags: map[string]complete.Predictor{}, | ||
} | ||
|
||
for _, f := range c.VisibleFlags() { | ||
for _, name := range f.Names() { | ||
name = fmt.Sprintf("%s%s", prefixFor(name), name) | ||
command.Flags[name] = ContextPredictor{f, ctx} | ||
} | ||
} | ||
|
||
if len(c.Args) > 0 || c.ShellComplete != nil { | ||
command.Args = ContextPredictor{c, ctx} | ||
} | ||
|
||
return command | ||
} | ||
|
||
func (c *Command) PredictArgs(ctx *Context, prefix string) []string { | ||
if c.ShellComplete != nil { | ||
return c.ShellComplete(ctx, prefix) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
type Predictor interface { | ||
PredictArgs(*Context, string) []string | ||
} | ||
|
||
// ContextPredictor determines what terms can follow a command or a flag | ||
// It is used for autocompletion, given the last word in the already completed | ||
// command line, what words can complete it. | ||
type ContextPredictor struct { | ||
predictor Predictor | ||
ctx *Context | ||
} | ||
|
||
// Predict invokes the predict function and implements the Predictor interface | ||
func (p ContextPredictor) Predict(prefix string) []string { | ||
return p.predictor.PredictArgs(p.ctx, prefix) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
//go:build darwin || linux || freebsd || openbsd | ||
|
||
package console | ||
|
||
import ( | ||
"bytes" | ||
"embed" | ||
"fmt" | ||
"os" | ||
"path" | ||
"strings" | ||
"text/template" | ||
|
||
"github.com/pkg/errors" | ||
"github.com/symfony-cli/terminal" | ||
) | ||
|
||
// completionTemplates holds our shell completions templates. | ||
// | ||
//go:embed resources/completion.* | ||
var completionTemplates embed.FS | ||
|
||
var shellAutoCompleteInstallCommand = &Command{ | ||
Category: "self", | ||
Name: "completion", | ||
Aliases: []*Alias{ | ||
{Name: "completion"}, | ||
}, | ||
Usage: "Dumps the completion script for the current shell", | ||
ShellComplete: func(*Context, string) []string { | ||
return []string{"bash", "zsh", "fish"} | ||
}, | ||
Description: `The <info>{{.HelpName}}</> command dumps the shell completion script required | ||
to use shell autocompletion (currently, bash, zsh and fish completion are supported). | ||
<comment>Static installation | ||
-------------------</> | ||
Dump the script to a global completion file and restart your shell: | ||
<info>{{.HelpName}} {{ call .Shell }} | sudo tee {{ call .CompletionFile }}</> | ||
Or dump the script to a local file and source it: | ||
<info>{{.HelpName}} {{ call .Shell }} > completion.sh</> | ||
<comment># source the file whenever you use the project</> | ||
<info>source completion.sh</> | ||
<comment># or add this line at the end of your "{{ call .RcFile }}" file:</> | ||
<info>source /path/to/completion.sh</> | ||
<comment>Dynamic installation | ||
--------------------</> | ||
Add this to the end of your shell configuration file (e.g. <info>"{{ call .RcFile }}"</>): | ||
<info>eval "$({{.HelpName}} {{ call .Shell }})"</>`, | ||
DescriptionFunc: func(command *Command, application *Application) string { | ||
var buf bytes.Buffer | ||
|
||
tpl := template.Must(template.New("description").Parse(command.Description)) | ||
|
||
if err := tpl.Execute(&buf, struct { | ||
// allows to directly access any field from the command inside the template | ||
*Command | ||
Shell func() string | ||
RcFile func() string | ||
CompletionFile func() string | ||
}{ | ||
Command: command, | ||
Shell: guessShell, | ||
RcFile: func() string { | ||
switch guessShell() { | ||
case "fish": | ||
return "~/.config/fish/config.fish" | ||
case "zsh": | ||
return "~/.zshrc" | ||
default: | ||
return "~/.bashrc" | ||
} | ||
}, | ||
CompletionFile: func() string { | ||
switch guessShell() { | ||
case "fish": | ||
return fmt.Sprintf("/etc/fish/completions/%s.fish", application.HelpName) | ||
case "zsh": | ||
return fmt.Sprintf("$fpath[1]/_%s", application.HelpName) | ||
default: | ||
return fmt.Sprintf("/etc/bash_completion.d/%s", application.HelpName) | ||
} | ||
}, | ||
}); err != nil { | ||
panic(err) | ||
} | ||
|
||
return buf.String() | ||
}, | ||
Args: []*Arg{ | ||
{ | ||
Name: "shell", | ||
Description: `The shell type (e.g. "bash"), the value of the "$SHELL" env var will be used if this is not given`, | ||
Optional: true, | ||
}, | ||
}, | ||
Action: func(c *Context) error { | ||
shell := c.Args().Get("shell") | ||
if shell == "" { | ||
shell = guessShell() | ||
} | ||
|
||
templates, err := template.ParseFS(completionTemplates, "resources/*") | ||
if err != nil { | ||
return errors.WithStack(err) | ||
} | ||
|
||
if tpl := templates.Lookup(fmt.Sprintf("completion.%s", shell)); tpl != nil { | ||
return errors.WithStack(tpl.Execute(terminal.Stdout, c)) | ||
} | ||
|
||
var supportedShell []string | ||
|
||
for _, tmpl := range templates.Templates() { | ||
if tmpl.Tree == nil || tmpl.Root == nil { | ||
continue | ||
} | ||
supportedShell = append(supportedShell, strings.TrimLeft(path.Ext(tmpl.Name()), ".")) | ||
} | ||
|
||
if shell == "" { | ||
return errors.Errorf(`shell not detected, supported shells: "%s"`, strings.Join(supportedShell, ", ")) | ||
} | ||
|
||
return errors.Errorf(`shell "%s" is not supported, supported shells: "%s"`, shell, strings.Join(supportedShell, ", ")) | ||
}, | ||
} | ||
|
||
func guessShell() string { | ||
return path.Base(os.Getenv("SHELL")) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
//go:build !darwin && !linux && !freebsd && !openbsd | ||
// +build !darwin,!linux,!freebsd,!openbsd | ||
|
||
package console | ||
|
||
const HasAutocompleteSupport = false | ||
|
||
func IsAutocomplete(c *Command) bool { | ||
return false | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
//go:build darwin || linux || freebsd || openbsd | ||
// +build darwin linux freebsd openbsd | ||
|
||
package console | ||
|
||
const SupportsAutocomplete = true | ||
|
||
func IsAutocomplete(c *Command) bool { | ||
return c == autoCompleteCommand | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.