8000 feat: native autocomplete for `console` and `composer` by tucksaun · Pull Request #493 · symfony-cli/symfony-cli · GitHub
[go: up one dir, main page]

Skip to content

feat: native autocomplete for console and composer #493

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 0 additions & 19 deletions commands/completion.go

This file was deleted.

17 changes: 17 additions & 0 deletions commands/completion_others.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//go:build !darwin && !linux && !freebsd && !openbsd
// +build !darwin,!linux,!freebsd,!openbsd

package commands

import (
"github.com/posener/complete"
"github.com/symfony-cli/console"
)

func autocompleteComposerWrapper(context *console.Context, args complete.Args) []string {
return []string{}
}

func autocompleteSymfonyConsoleWrapper(context *console.Context, args complete.Args) []string {
return []string{}
}
83 changes: 83 additions & 0 deletions commands/completion_posix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
//go:build darwin || linux || freebsd || openbsd

package commands

import (
"embed"
"fmt"
"io"
"os"
"strconv"
"strings"

"github.com/posener/complete"
"github.com/symfony-cli/console"
"github.com/symfony-cli/symfony-cli/local/php"
"github.com/symfony-cli/terminal"
)

// completionTemplates holds our custom shell completions templates.
//
//go:embed resources/completion.*
var completionTemplates embed.FS

func init() {
// override console completion templates with our custom ones
console.CompletionTemplates = completionTemplates
}

func autocompleteSymfonyConsoleWrapper(context *console.Context, words complete.Args) []string {
args := buildSymfonyAutocompleteArgs("console", words)
// Composer does not support those options yet, so we only use them for Symfony Console
args = append(args, "-a1", fmt.Sprintf("-s%s", console.GuessShell()))

if executor, err := php.SymonyConsoleExecutor(args); err == nil {
os.Exit(executor.Execute(false))
}

return []string{}
}

func autocompleteComposerWrapper(context *console.Context, words complete.Args) []string {
args := buildSymfonyAutocompleteArgs("composer", words)
// Composer does not support multiple shell yet, so we only use the default one
args = append(args, "-sbash")

res := php.Composer("", args, []string{}, context.App.Writer, context.App.ErrWriter, io.Discard, terminal.Logger)
os.Exit(res.ExitCode())

// unreachable
return []string{}
}

func buildSymfonyAutocompleteArgs(wrappedCommand string, words complete.Args) []string {
current, err := strconv.Atoi(os.Getenv("CURRENT"))
if err != nil {
current = 1
} else {
// we decrease one position corresponding to `symfony` command
current -= 1
}

args := make([]string, 0, len(words.All))
// build the inputs command line that Symfony expects
for _, input := range words.All {
if input = strings.TrimSpace(input); input != "" {

// remove quotes from typed values
quote := input[0]
if quote == '\'' || quote == '"' {
input = strings.TrimPrefix(input, string(quote))
input = strings.TrimSuffix(input, string(quote))
}

args = append(args, fmt.Sprintf("-i%s", input))
}
}

return append([]string{
"_complete", "--no-interaction",
fmt.Sprintf("-c%d", current),
fmt.Sprintf("-i%s", wrappedCommand),
}, args...)
}
57 changes: 43 additions & 14 deletions commands/resources/completion.bash
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,6 @@
# - https://github.com/scop/bash-completion/blob/master/completions/sudo
#

# this wrapper function allows us to let Symfony knows how to call the
# `bin/console` using the Symfony CLI binary (to ensure the right env and PHP
# versions are used)
_{{ .App.HelpName }}_console() {
# shellcheck disable=SC2068
{{ .CurrentBinaryInvocation }} console $@
}

_complete_{{ .App.HelpName }}() {

# Use the default completion for shell redirect operators.
Expand All @@ -45,19 +37,56 @@ _complete_{{ .App.HelpName }}() {
for (( i=1; i <= COMP_CWORD; i++ )); do
if [[ "${COMP_WORDS[i]}" != -* ]]; then
case "${COMP_WORDS[i]}" in
console)
_SF_CMD="_{{ .App.HelpName }}_console" _command_offset $i
return
;;
composer{{range $name := (.App.Command "php").Names }}|{{$name}}{{end}}{{range $name := (.App.Command "run").Names }}|{{$name}}{{end}})
{{range $i, $name := (.App.Command "php").Names }}{{if $i}}|{{end}}{{$name}}{{end}}{{range $name := (.App.Command "run").Names }}|{{$name}}{{end}})
_command_offset $i
return
;;
esac;
fi
done

IFS=$'\n' COMPREPLY=( $(COMP_LINE="${COMP_LINE}" COMP_POINT="${COMP_POINT}" COMP_DEBUG="$COMP_DEBUG" {{ .CurrentBinaryPath }} self:autocomplete) )
# Use newline as only separator to allow space in completion values
IFS=$'\n'

local cur prev words cword
_get_comp_words_by_ref -n := cur prev words cword

local sfcomplete
if sfcomplete=$(COMP_LINE="${COMP_LINE}" COMP_POINT="${COMP_POINT}" COMP_DEBUG="$COMP_DEBUG" CURRENT="$cword" {{ .CurrentBinaryPath }} self:autocomplete 2>&1); then
local quote suggestions
quote=${cur:0:1}

# Use single quotes by default if suggestions contains backslash (FQCN)
if [ "$quote" == '' ] && [[ "$sfcomplete" =~ \\ ]]; then
quote=\'
fi

if [ "$quote" == \' ]; then
# single quotes: no additional escaping (does not accept ' in values)
suggestions=$(for s in $sfcomplete; do printf $'%q%q%q\n' "$quote" "$s" "$quote"; done)
elif [ "$quote" == \" ]; then
# double quotes: double escaping for \ $ ` "
suggestions=$(for s in $sfcomplete; do
s=${s//\\/\\\\}
s=${s//\$/\\\$}
s=${s//\`/\\\`}
s=${s//\"/\\\"}
printf $'%q%q%q\n' "$quote" "$s" "$quote";
done)
else
# no quotes: double escaping
suggestions=$(for s in $sfcomplete; do printf $'%q\n' $(printf '%q' "$s"); done)
fi
COMPREPLY=($(IFS=$'\n' compgen -W "$suggestions" -- $(printf -- "%q" "$cur")))
__ltrim_colon_completions "$cur"
else
if [[ "$sfcomplete" != *"Command \"_complete\" is not defined."* ]]; then
>&2 echo
>&2 echo $sfcomplete
fi

return 1
fi
}

complete -F _complete_{{ .App.HelpName }} {{ .App.HelpName }}
13 changes: 2 additions & 11 deletions commands/resources/completion.fish
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,10 @@ function __complete_{{ .App.HelpName }}
set -lx COMP_LINE (commandline -cp)
test -z (commandline -ct)
and set COMP_LINE "$COMP_LINE "
set -x CURRENT (count (commandline -oc))
{{ .CurrentBinaryInvocation }} self:autocomplete
end

# this wrapper function allows us to call Symfony autocompletion letting it
# knows how to call the `bin/console` using the Symfony CLI binary (to ensure
# the right env and PHP versions are used)
function __complete_{{ .App.HelpName }}_console
set -x _SF_CMD "{{ .CurrentBinaryInvocation }}" "console"
__fish_complete_subcommand
end

complete -f -c '{{ .App.HelpName }}' -n "__fish_seen_subcommand_from console" -a '(__complete_{{ .App.HelpName }}_console)' -f
complete -f -c '{{ .App.HelpName }}' -n "__fish_seen_subcommand_from composer" -a '(__fish_complete_subcommand)'
complete -f -c '{{ .App.HelpName }}' -n "__fish_seen_subcommand_from {{range $i, $name := (.App.Command "php").Names }}{{if $i}} {{end}}{{$name}}{{end}}" -a '(__fish_complete_subcommand)'
complete -f -c '{{ .App.HelpName }}' -n "__fish_seen_subcommand_from {{range $i, $name := (.App.Command "run").Names }}{{if $i}} {{end}}{{$name}}{{end}}" -a '(__fish_complete_subcommand --fcs-skip=2)'
complete -f -c '{{ .App.HelpName }}' -n "not __fish_seen_subcommand_from console composer {{range $i, $name := (.App.Command "php").Names }}{{if $i}} {{end}}{{$name}}{{end}} {{range $i, $name := (.App.Command "run").Names }}{{if $i}} {{end}}{{$name}}{{end}}" -a '(__complete_{{ .App.HelpName }})'
complete -f -c '{{ .App.HelpName }}' -n "not __fish_seen_subcommand_from {{range $i, $name := (.App.Command "php").Names }}{{if $i}} {{end}}{{$name}}{{end}} {{range $i, $name := (.App.Command "run").Names }}{{if $i}} {{end}}{{$name}}{{end}}" -a '(__complete_{{ .App.HelpName }})'
22 changes: 8 additions & 14 deletions commands/resources/completion.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,6 @@
# - https://stackoverflow.com/a/13547531
#

# this wrapper function allows us to let Symfony knows how to call the
# `bin/console` using the Symfony CLI binary (to ensure the right env and PHP
# versions are used)
_{{ .App.HelpName }}_console() {
# shellcheck disable=SC2068
{{ .CurrentBinaryInvocation }} console $@
}

_complete_{{ .App.HelpName }}() {
local lastParam flagPrefix requestComp out comp
local -a completions
Expand All @@ -56,13 +48,10 @@ _complete_{{ .App.HelpName }}() {
for ((i = 1; i <= $#words; i++)); do
if [[ "${words[i]}" != -* ]]; then
case "${words[i]}" in
console)
shift words
console|composer)
(( CURRENT-- ))
_SF_CMD="_{{ .App.HelpName }}_console" _normal
return
;;
composer{{range $name := (.App.Command "php").Names }}|{{$name}}{{end}})
{{range $i, $name := (.App.Command "php").Names }}{{if $i}}|{{end}}{{$name}}{{end}})
shift words
(( CURRENT-- ))
_normal
Expand All @@ -82,11 +71,16 @@ _complete_{{ .App.HelpName }}() {

while IFS='\n' read -r comp; do
if [ -n "$comp" ]; then
# If requested, completions are returned with a description.
# The description is preceded by a TAB character.
# For zsh's _describe, we need to use a : instead of a TAB.
# We first need to escape any : as part of the completion itself.
comp=${comp//:/\\:}
local tab=$(printf '\t')
comp=${comp//$tab/:}
completions+=${comp}
fi
done < <(COMP_LINE="$words" ${words[0]} ${_SF_CMD:-${words[1]}} self:autocomplete)
done < <(COMP_LINE="$words" CURRENT="$CURRENT" ${words[0]} ${_SF_CMD:-${words[1]}} self:autocomplete)

# Let inbuilt _describe handle completions
eval _describe "completions" completions $flagPrefix
Expand Down
6 changes: 4 additions & 2 deletions commands/wrappers.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ var (
Hidden: console.Hide,
// we use an alias to avoid the command being shown in the help but
// still be available for completion
Aliases: []*console.Alias{{Name: "composer"}},
Aliases: []*console.Alias{{Name: "composer"}},
ShellComplete: autocompleteComposerWrapper,
Action: func(c *console.Context) error {
return console.IncorrectUsageError{ParentError: errors.New(`This command can only be run as "symfony composer"`)}
},
Expand All @@ -43,8 +44,9 @@ var (
// still be available for completion
Aliases: []*console.Alias{{Name: "console"}},
Action: func(c *console.Context) error {
return console.IncorrectUsageError{ParentError: errors.New(`This command can only be run as "symfony console"`)}
return errors.New(`No Symfony console detected to run "symfony console"`)
},
ShellComplete: autocompleteSymfonyConsoleWrapper,
}
phpWrapper = &console.Command{
Usage: "Runs the named binary using the configured PHP version",
Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ require (
github.com/nxadm/tail v1.4.11
github.com/olekukonko/tablewriter v0.0.5
github.com/pkg/errors v0.9.1
github.com/posener/complete v1.2.3
github.com/rjeczalik/notify v0.9.3
github.com/rs/xid v1.5.0
github.com/rs/zerolog v1.33.0
Expand All @@ -25,7 +26,7 @@ require (
github.com/soheilhy/cmux v0.1.5
github.com/stoicperlman/fls v0.0.0-20171222144224-f073b7a01081
github.com/symfony-cli/cert v1.0.6
github.com/symfony-cli/console v1.1.3
github.com/symfony-cli/console v1.1.4
github.com/symfony-cli/phpstore v1.0.12
github.com/symfony-cli/terminal v1.0.7
golang.org/x/sync v0.7.0
Expand Down Expand Up @@ -65,7 +66,6 @@ require (
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
github.com/posener/complete v1.2.3 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rogpeppe/go-internal v1.12.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,8 @@ github.com/symfony-cli/console v1.1.2 h1:YVQbl4i03cE0r3QJ/RJs7LWaC3pEy2JI4qhFfL6
github.com/symfony-cli/console v1.1.2/go.mod h1:AB4ZxA593cyS/1NhwnDEUChIPaGuddFqooipam1vyS8=
github.com/symfony-cli/console v1.1.3 h1:ejzr9LdNe7d7FLIpeTyZm89nSgnlPhaqK7IPeyglV9o=
github.com/symfony-cli/console v1.1.3/go.mod h1:AB4ZxA593cyS/1NhwnDEUChIPaGuddFqooipam1vyS8=
github.com/symfony-cli/console v1.1.4 h1:A/rzNY8HiZd4r6ip2H2HCtnxwYFdC87eYnPL9H/RucM=
github.com/symfony-cli/console v1.1.4/go.mod h1:AB4ZxA593cyS/1NhwnDEUChIPaGuddFqooipam1vyS8=
github.com/symfony-cli/phpstore v1.0.12 h1:2mKJrDielSCW+7B+63w6HebmSBcB4qV7uuvNrIjLkoA=
github.com/symfony-cli/phpstore v1.0.12/go.mod h1:U29bdJBPs9p28PzLIRKfKfKkaiH0kacdyufl3eSB1d4=
github.com/symfony-cli/terminal v1.0.7 h1:57L9PUTE2cHfQtP8Ti8dyiiPEYlQ1NBIDpMJ3RPEGPc=
Expand Down
27 changes: 27 additions & 0 deletions local/php/symfony.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package php

import (
"os"

"github.com/pkg/errors"
)

// ComposerExecutor returns an Executor prepared to run Symfony Console.
// It returns an error if no console binary is found.
func SymonyConsoleExecutor(args []string) (*Executor, error) {
consolePath := "bin/console"

if _, err := os.Stat(consolePath); err != nil {
// Fallback to app/console for projects created with older versions of Symfony
consolePath = "app/console"

if _, err2 := os.Stat(consolePath); err2 != nil {
return nil, errors.WithStack(err)
}
}

return &Executor{
BinName: "php",
Args: append([]string{"php", consolePath}, args...),
}, nil
}
12 changes: 3 additions & 9 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,16 +73,10 @@ func main() {
}
// called via "symfony console"?
if len(args) >= 2 && args[1] == "console" {
args[1] = "bin/console"
if _, err := os.Stat("app/console"); err == nil {
args[1] = "app/console"
if executor, err := php.SymonyConsoleExecutor(args[2:]); err == nil {
executor.ExtraEnv = getCliExtraEnv()
os.Exit(executor.Execute(false))
}
e := &php.Executor{
BinName: "php",
Args: args,
ExtraEnv: getCliExtraEnv(),
}
os.Exit(e.Execute(false))
}
// called via "symfony composer"?
if len(args) >= 2 && args[1] == "composer" {
Expand Down
0