From 943498b9050004552f2badd6601a5559ea5a5ee0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcos=20Guimar=C3=A3es?= Date: Fri, 14 Feb 2020 14:16:42 -0300 Subject: [PATCH] auto completion local --- cmd/main.go | 2 +- pkg/autocomplete/default_autocomplete.go | 151 +++++++++++++++++--- pkg/autocomplete/template.go | 171 +++++++++++++++++++++++ 3 files changed, 304 insertions(+), 20 deletions(-) create mode 100644 pkg/autocomplete/template.go diff --git a/cmd/main.go b/cmd/main.go index 628049ab3..fcc087986 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -49,7 +49,7 @@ func main() { credManager := credential.NewDefaultManager(env.ServerUrl, http.DefaultClient, sessionManager) userManager := ruser.NewDefaultManager(env.ServerUrl, http.DefaultClient, sessionManager) workspaceManager := workspace.NewDefaultManager(ritchieHomePath, env.ServerUrl, http.DefaultClient, treeManager, gitManager, credManager, sessionManager) - autocompleteManager := autocomplete.NewDefaultManager(env.ServerUrl, http.DefaultClient) + autocompleteManager := autocomplete.NewDefaultManager(env.ServerUrl, ritchieHomePath, http.DefaultClient) ctxManager := context.NewDefaultManager(sessionManager) metricsManager := metrics.NewDefaultManager(env.ServerUrl, &http.Client{Timeout: 2 * time.Second}, sessionManager) credResolver := envcredential.NewResolver(credManager) diff --git a/pkg/autocomplete/default_autocomplete.go b/pkg/autocomplete/default_autocomplete.go index 9283f0fdf..1747fd5fd 100644 --- a/pkg/autocomplete/default_autocomplete.go +++ b/pkg/autocomplete/default_autocomplete.go @@ -3,47 +3,160 @@ package autocomplete import ( "errors" "fmt" - "io/ioutil" + "github.com/ZupIT/ritchie-cli/pkg/tree" "net/http" + "strings" "github.com/ZupIT/ritchie-cli/pkg/slice/sliceutil" ) -const pathUrl = "%s/auto-complete/%s" +const ( + binaryName string = "rit" + lineCommand string = " commands+=(\"${command}\")" + firstLevel string = "root" + bash string = "bash" + zsh string = "zsh" +) type defaultManager struct { - serverURL string - httpClient *http.Client + serverURL string + httpClient *http.Client + ritchieHome string } -func NewDefaultManager(serverURL string, c *http.Client) *defaultManager { - return &defaultManager{serverURL: serverURL, httpClient: c} +type ( + BashCommand struct { + LastCommand string + RootCommand string + Commands string + Level int + } + + Command struct { + Content []string + Before string + } +) + +func NewDefaultManager(serverURL, ritchieHome string, c *http.Client) *defaultManager { + return &defaultManager{serverURL: serverURL, httpClient: c, ritchieHome: ritchieHome} } func (d *defaultManager) Handle(shellName string) (string, error) { - if !sliceutil.Contains(supportedAutocomplete(), shellName) { return "", errors.New("autocomplete for this terminal is not supported") } - - url := fmt.Sprintf(pathUrl, d.serverURL, shellName) - req, _ := http.NewRequest(http.MethodGet, url, nil) - - resp, err := d.httpClient.Do(req) + treeManager := tree.NewDefaultManager(d.ritchieHome, "", nil, nil) + tree, err := treeManager.GetLocalTree() if err != nil { return "", err } + autoCompletion := "" + switch shellName { + case bash: + autoCompletion = fmt.Sprintf("%s\n%s", "#!/bin/bash", loadToBash(*tree)) + case zsh: + autoCompletion = loadToZsh(*tree) + } + return autoCompletion, nil +} - defer resp.Body.Close() +func supportedAutocomplete() []string { + return []string{bash, zsh} +} - bodyBytes, err := ioutil.ReadAll(resp.Body) - if err != nil { - return "", err +func loadToBash(tree tree.Representation) string { + autoComplete := autoCompletionBash + autoComplete = strings.Replace(autoComplete, "{{BinaryName}}", binaryName, -1) + autoComplete = strings.Replace(autoComplete, "{{DynamicCode}}", loadDynamicCommands(tree), 1) + return autoComplete +} + +func loadToZsh(tree tree.Representation) string { + autoComplete := autoCompletionZsh + autoComplete = strings.Replace(autoComplete, "{{BinaryName}}", binaryName, -1) + autoComplete = strings.Replace(autoComplete, "{{AutoCompleteBash}}", loadToBash(tree), 1) + return autoComplete +} + +func loadDynamicCommands(tree tree.Representation) string { + commands := tree.Commands + commandString := command + mapCommand := loadCommands(commands) + bashCommands := loadBashCommands(mapCommand) + + allCommands := "" + for _, bashCommand := range bashCommands { + functionName := formatterFunctionName(bashCommand.RootCommand) + command := strings.Replace(commandString, "{{RootCommand}}", bashCommand.RootCommand, -1) + command = strings.Replace(command, "{{LastCommand}}", bashCommand.LastCommand, -1) + command = strings.Replace(command, "{{FunctionName}}", functionName, -1) + allCommands += strings.Replace(command, "{{Commands}}", bashCommand.Commands, -1) } + return allCommands +} - return string(bodyBytes), nil +func formatterFunctionName(functionName string) string { + functionParts := strings.Split(functionName, "_") + if len(functionParts) > 2 { + functionName = functionParts[len(functionParts)-2] + "_" + functionParts[len(functionParts)-1] + } + return functionName } -func supportedAutocomplete() []string { - return []string{"bash", "zsh"} +func loadCommands(commands []tree.Command) map[string]Command { + commandsMap := make(map[string]Command) + for _, command := range commands { + addValueMap(&commandsMap, command.Parent, command.Usage, command.Parent) + } + commandsMapResponse := make(map[string]Command) + for key, value := range commandsMap { + commandsMapResponse[key] = value + for _, v := range value.Content { + newKey := key + "_" + v + if _, ok := commandsMap[newKey]; !ok { + commandsMapResponse[newKey] = Command{ + Content: nil, + Before: newKey, + } + } + } + } + return commandsMapResponse +} + +func loadBashCommands(mapCommands map[string]Command) []BashCommand { + var bashCommands []BashCommand + for key, value := range mapCommands { + rootCommand := key + level := len(strings.Split(key, "_")) + commands := "" + for _, valueEntry := range value.Content { + commands += strings.Replace(lineCommand, "${command}", valueEntry, -1) + "\n" + } + if rootCommand == firstLevel { + rootCommand = fmt.Sprintf("%s_%s", binaryName, rootCommand) + } + bashCommands = append(bashCommands, BashCommand{ + RootCommand: rootCommand, + Commands: commands, + LastCommand: loadLastCommand(key), + Level: level, + }, + ) + } + return bashCommands +} + +func loadLastCommand(rootCommand string) string { + splitRootCommand := strings.Split(rootCommand, "_") + return splitRootCommand[len(splitRootCommand)-1] +} + +func addValueMap(mapCommand *map[string]Command, key string, value string, before string) { + i := *mapCommand + a := i[key] + a.Content = append(a.Content, value) + a.Before = before + i[key] = a } diff --git a/pkg/autocomplete/template.go b/pkg/autocomplete/template.go new file mode 100644 index 000000000..48a2eada8 --- /dev/null +++ b/pkg/autocomplete/template.go @@ -0,0 +1,171 @@ +package autocomplete + +const ( + command string = `_{{FunctionName}}() +{ + last_command="{{LastCommand}}" + + commands=() +{{Commands}} +} +` + + + + autoCompletionBash string = ` +{{DynamicCode}} + +__{{BinaryName}}_init_completion() +{ + COMPREPLY=() + _get_comp_words_by_ref "$@" cur prev words cword +} + +__{{BinaryName}}_index_of_word() +{ + local w word=$1 + shift + index=0 + for w in "$@"; do + [[ $w = "$word" ]] && return + index=$((index+1)) + done + index=-1 +} + +__{{BinaryName}}_contains_word() +{ + local w word=$1; shift + for w in "$@"; do + [[ $w = "$word" ]] && return + done + return 1 +} + +__{{BinaryName}}_handle_command() +{ + + local next_command + if [[ -n ${last_command} ]]; then + next_command="_${last_command}_${words[c]//:/__}" + else + if [[ $c -eq 0 ]]; then + next_command="_{{BinaryName}}_root" + else + next_command="_${words[c]//:/__}" + fi + fi + c=$((c+1)) + declare -F "$next_command" >/dev/null && $next_command +} + + +__{{BinaryName}}_handle_reply() +{ + local completions + completions=("${commands[@]}") + COMPREPLY=( $(compgen -W "${completions[*]}" -- "$cur") ) + +} + +__{{BinaryName}}_handle_word() +{ + if [[ $c -ge $cword ]]; then + __{{BinaryName}}_handle_reply + return + fi + if __{{BinaryName}}_contains_word "${words[c]}" "${commands[@]}"; then + __{{BinaryName}}_handle_command + fi + + __{{BinaryName}}_handle_word +} + +__start_{{BinaryName}}() +{ + local cur prev words cword + __{{BinaryName}}_init_completion -n "=" || return + + local c=0 + local commands=("{{BinaryName}}") + local last_command + + __{{BinaryName}}_handle_word +} + +complete -F __start_{{BinaryName}} {{BinaryName}}` + + + + + autoCompletionZsh string = `#compdef {{BinaryName}} + +__{{BinaryName}}_bash_source() { + alias shopt=':' + alias _expand=_bash_expand + alias _complete=_bash_comp + emulate -L sh + setopt kshglob noshglob braceexpand + + source "$@" +} + +__{{BinaryName}}_compgen() { + local completions w + completions=( $(compgen "$@") ) || return $? + + while [[ "$1" = -* && "$1" != -- ]]; do + shift + shift + done + if [[ "$1" == -- ]]; then + shift + fi + for w in "${completions[@]}"; do + if [[ "${w}" = "$1"* ]]; then + echo "${w}" + fi + done +} + +__{{BinaryName}}_compopt() { + true +} + +__{{BinaryName}}_get_comp_words_by_ref() { + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[${COMP_CWORD}-1]}" + words=("${COMP_WORDS[@]}") + cword=("${COMP_CWORD[@]}") +} + +autoload -U +X bashcompinit && bashcompinit + +LWORD='[[:<:]]' +RWORD='[[:>:]]' +if sed --help 2>&1 | grep -q GNU; then + LWORD='\<' + RWORD='\>' +fi + +__{{BinaryName}}_convert_bash_to_zsh() { + sed \ + -e 's/declare -F/whence -w/' \ + -e 's/_get_comp_words_by_ref "\$@"/_get_comp_words_by_ref "\$*"/' \ + -e 's/local \([a-zA-Z0-9_]*\)=/local \1; \1=/' \ + -e "s/${LWORD}_get_comp_words_by_ref${RWORD}/__{{BinaryName}}_get_comp_words_by_ref/g" \ + -e "s/${LWORD}compgen${RWORD}/__{{BinaryName}}_compgen/g" \ + -e "s/${LWORD}compopt${RWORD}/__{{BinaryName}}_compopt/g" \ + -e "s/${LWORD}declare${RWORD}/builtin declare/g" \ + <<'BASH_COMPLETION_EOF' + +{{AutoCompleteBash}} + +# ex: ts=4 sw=4 et filetype=sh +BASH_COMPLETION_EOF +} + +__{{BinaryName}}_bash_source <(__{{BinaryName}}_convert_bash_to_zsh) + +_complete {{BinaryName}} 2>/dev/null` +)