Skip to content
Open
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
46 changes: 33 additions & 13 deletions example/cmd/_test/zsh.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,51 @@
function _example_completion {
local words=${words[@]:0:$CURRENT}
local IFS=$'\n'

# shellcheck disable=SC2086,SC2154,SC2155
local completion_input
if echo ${words}"''" | xargs echo 2>/dev/null > /dev/null; then
local lines="$(echo ${words}"''" | CARAPACE_COMPLINE="${words}" CARAPACE_ZSH_HASH_DIRS="$(hash -d)" xargs example _carapace zsh )"
elif echo ${words} | sed "s/\$/'/" | xargs echo 2>/dev/null > /dev/null; then
local lines="$(echo ${words} | sed "s/\$/'/" | CARAPACE_COMPLINE="${words}" CARAPACE_ZSH_HASH_DIRS="$(hash -d)" xargs example _carapace zsh)"
completion_input="${words}''"
elif echo "${words[1,-2]} ${words[-1]}'" | xargs echo 2>/dev/null > /dev/null; then
completion_input="${words[1,-2]} ${words[-1]}'"
else
local lines="$(echo ${words} | sed 's/$/"/' | CARAPACE_COMPLINE="${words}" CARAPACE_ZSH_HASH_DIRS="$(hash -d)" xargs example _carapace zsh)"
completion_input="${words[1,-2]} ${words[-1]}\\"
fi


local lines
lines="$(echo "${completion_input}" | CARAPACE_COMPLINE="${words}" CARAPACE_ZSH_HASH_DIRS="$(hash -d)" xargs example _carapace zsh)"

local zstyle message data
IFS=$'\001' read -r -d '' zstyle message data <<<"${lines}"
# shellcheck disable=SC2154
zstyle ":completion:${curcontext}:*" list-colors "${zstyle}"
zstyle ":completion:${curcontext}:*" group-name ''
[ -z "$message" ] || _message -r "${message}"

local block tag displays values displaysArr valuesArr
local block tag suffix displays values
local -A tags
while IFS=$'\002' read -r -d $'\002' block; do
IFS=$'\003' read -r -d '' tag displays values <<<"${block}"
# shellcheck disable=SC2034
IFS=$'\n' read -r -d $'\004' -A displaysArr <<<"${displays}"$'\004'
IFS=$'\n' read -r -d $'\004' -A valuesArr <<<"${values}"$'\004'

[[ ${#valuesArr[@]} -gt 1 ]] && _describe -t "${tag}" "${tag}" displaysArr valuesArr -Q -S ''
IFS=$'\003' read -r -d '' tag suffix displays values <<<"${block}"

local -a displaysArr=("${(f@)displays}")
local -a valuesArr=("${(f@)values}")

local -a describe_args
if [[ -z ${tags[$tag]} ]]; then
describe_args=(-t "${tag}" "${tag}")
tags[$tag]=1
else
describe_args=(-t "${tag}" "")
fi

local separators=" /,.':@="
if [[ "$suffix" == "" ]]; then
_describe "${describe_args[@]}" displaysArr valuesArr -Q -S ' ' -r "${separators}0-9a-zA-Z"
elif [[ "$separators" == *"$suffix"* ]]; then
_describe "${describe_args[@]}" displaysArr valuesArr -Q -S "$suffix" -r ' '
else
_describe "${describe_args[@]}" displaysArr valuesArr -Q -S "$suffix" -r "0-9a-zA-Z"
fi
done <<<"${data}"
}
compquote '' 2>/dev/null && _example_completion
Expand Down
6 changes: 4 additions & 2 deletions internal/common/suffix.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ func (sm *SuffixMatcher) UnmarshalJSON(data []byte) (err error) {

type ByRune []rune

func (r ByRune) Len() int { return len(r) }
func (r ByRune) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
func (r ByRune) Len() int { return len(r) }

func (r ByRune) Swap(i, j int) { r[i], r[j] = r[j], r[i] }

func (r ByRune) Less(i, j int) bool { return r[i] < r[j] }
135 changes: 92 additions & 43 deletions internal/shell/zsh/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@ import (
"fmt"
"regexp"
"strings"
"time"
"unicode"

shlex "github.com/carapace-sh/carapace-shlex"
"github.com/carapace-sh/carapace/internal/common"
"github.com/carapace-sh/carapace/internal/env"
"github.com/carapace-sh/carapace/internal/log"
)

var sanitizer = strings.NewReplacer(
Expand All @@ -17,7 +20,7 @@ var sanitizer = strings.NewReplacer(
)

var quotingReplacer = strings.NewReplacer(
`'`, `'\''`,
`'`, `'''`,
)

var quotingEscapingReplacer = strings.NewReplacer(
Expand Down Expand Up @@ -92,6 +95,11 @@ const (

// ActionRawValues formats values for zsh
func ActionRawValues(currentWord string, meta common.Meta, values common.RawValues) string {
start := time.Now()
defer func() {
log.LOG.Printf("zsh action processing took %v", time.Since(start))
}()

splitted, err := shlex.Split(env.Compline())
state := DEFAULT_STATE
if err == nil {
Expand All @@ -100,63 +108,104 @@ func ActionRawValues(currentWord string, meta common.Meta, values common.RawValu
switch {
case regexp.MustCompile(`^'$|^'.*[^']$`).MatchString(rawValue):
state = QUOTING_STATE
case regexp.MustCompile(`^"$|^".*[^"]$`).MatchString(rawValue):
case regexp.MustCompile(`^"$|^\".*[^\"]$`).MatchString(rawValue):
state = QUOTING_ESCAPING_STATE
case regexp.MustCompile(`^".*"$`).MatchString(rawValue):
case regexp.MustCompile(`^\".*\"$`).MatchString(rawValue):
state = FULL_QUOTING_ESCAPING_STATE
case regexp.MustCompile(`^'.*'$`).MatchString(rawValue):
state = FULL_QUOTING_STATE
}
}

tagGroup := make([]string, 0)
values.EachTag(func(tag string, values common.RawValues) {
vals := make([]string, len(values))
displays := make([]string, len(values))
for index, val := range values {
value := sanitizer.Replace(val.Value)

switch state {
case QUOTING_ESCAPING_STATE:
value = quotingEscapingReplacer.Replace(value)
value = describeReplacer.Replace(value)
value = value + `"`
case QUOTING_STATE:
value = quotingReplacer.Replace(value)
value = describeReplacer.Replace(value)
value = value + `'`
case FULL_QUOTING_ESCAPING_STATE:
value = quotingEscapingReplacer.Replace(value)
value = describeReplacer.Replace(value)
case FULL_QUOTING_STATE:
value = quotingReplacer.Replace(value)
value = describeReplacer.Replace(value)
default:
value = quoteValue(value)
value = describeReplacer.Replace(value)
}
tagGroups := make([]string, 0)
values.EachTag(func(tag string, tagValues common.RawValues) {
for suffix, suffixedValues := range groupValuesBySuffix(tagValues, meta, state) {
vals := make([]string, len(suffixedValues))
displays := make([]string, len(suffixedValues))

for index, val := range suffixedValues {
value := sanitizer.Replace(val.Value)
if suffix != " " && suffix != "" {
value = strings.TrimSuffix(value, suffix)
}

if !meta.Nospace.Matches(val.Value) {
switch state {
case FULL_QUOTING_ESCAPING_STATE, FULL_QUOTING_STATE: // nospace workaround
case QUOTING_ESCAPING_STATE:
value = quotingEscapingReplacer.Replace(value)
value = describeReplacer.Replace(value)
value = value + `"`
case QUOTING_STATE:
value = quotingReplacer.Replace(value)
value = describeReplacer.Replace(value)
value = value + `'`
case FULL_QUOTING_ESCAPING_STATE:
value = quotingEscapingReplacer.Replace(value)
value = describeReplacer.Replace(value)
case FULL_QUOTING_STATE:
value = quotingReplacer.Replace(value)
value = describeReplacer.Replace(value)
default:
value += " "
value = quoteValue(value)
value = describeReplacer.Replace(value)
}

display := sanitizer.Replace(val.Display)
display = describeReplacer.Replace(display) // TODO check if this needs to be applied to description as well
description := sanitizer.Replace(val.Description)

vals[index] = value
if strings.TrimSpace(description) == "" {
displays[index] = display
} else {
displays[index] = fmt.Sprintf("%v:%v", display, description)
}
}
tagGroups = append(tagGroups, strings.Join([]string{tag, suffix, strings.Join(displays, "\n"), strings.Join(vals, "\n")}, "\003"))
}
})
return fmt.Sprintf("%v\001%v\001%v\002", zstyles{values}.Format(), message{meta}.Format(), strings.Join(tagGroups, "\002"))
}

display := sanitizer.Replace(val.Display)
display = describeReplacer.Replace(display) // TODO check if this needs to be applied to description as well
description := sanitizer.Replace(val.Description)
func groupValuesBySuffix(values common.RawValues, meta common.Meta, state state) map[string]common.RawValues {
groups := make(map[string]common.RawValues)
for _, val := range values {
suffix := ""
var removableSuffix bool

// If the last character is not an alphanumeric character, we assume that
// this character should be removed if the user inserts either a space, the
// same character or any type separator character ( /,.:@=)
if len(val.Value) > 0 {
lastChar := val.Value[len(val.Value)-1:]
removableSuffix = !isAlphaNumeric(rune(lastChar[len(lastChar)-1]))
}

vals[index] = value
// If we have matched the value suffix against carapace-registered suffixes,
// and the suffix is not an alphanumeric, then we should register the suffix
// as removable by ZSH (ie. ZSH will handle automatic insert/erase)
if meta.Nospace.Matches(val.Value) && len(val.Value) > 0 && removableSuffix {
lastChar := val.Value[len(val.Value)-1:]
suffix = lastChar
}

if strings.TrimSpace(description) == "" {
displays[index] = display
} else {
displays[index] = fmt.Sprintf("%v:%v", display, description)
if suffix == "" {
if !meta.Nospace.Matches(val.Value) {
switch state {
case FULL_QUOTING_ESCAPING_STATE, FULL_QUOTING_STATE: // nospace workaround
default:
suffix = " "
}
}
}
tagGroup = append(tagGroup, strings.Join([]string{tag, strings.Join(displays, "\n"), strings.Join(vals, "\n")}, "\003"))
})
return fmt.Sprintf("%v\001%v\001%v\001", zstyles{values}.Format(), message{meta}.Format(), strings.Join(tagGroup, "\002")+"\002")

if _, ok := groups[suffix]; !ok {
groups[suffix] = make(common.RawValues, 0)
}
groups[suffix] = append(groups[suffix], val)
}
return groups
}

func isAlphaNumeric(suffix rune) bool {
return unicode.IsDigit(suffix) || unicode.IsNumber(suffix) || unicode.IsLetter(suffix)
}
51 changes: 36 additions & 15 deletions internal/shell/zsh/snippet.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ package zsh
import (
"fmt"

"github.com/carapace-sh/carapace/pkg/uid"
"github.com/spf13/cobra"

"github.com/carapace-sh/carapace/pkg/uid"
)

// Snippet creates the zsh completion script
Expand All @@ -14,34 +15,54 @@ func Snippet(cmd *cobra.Command) string {
function _%v_completion {
local words=${words[@]:0:$CURRENT}
local IFS=$'\n'

# shellcheck disable=SC2086,SC2154,SC2155
local completion_input
if echo ${words}"''" | xargs echo 2>/dev/null > /dev/null; then
local lines="$(echo ${words}"''" | CARAPACE_COMPLINE="${words}" CARAPACE_ZSH_HASH_DIRS="$(hash -d)" xargs %v _carapace zsh )"
elif echo ${words} | sed "s/\$/'/" | xargs echo 2>/dev/null > /dev/null; then
local lines="$(echo ${words} | sed "s/\$/'/" | CARAPACE_COMPLINE="${words}" CARAPACE_ZSH_HASH_DIRS="$(hash -d)" xargs %v _carapace zsh)"
completion_input="${words}''"
elif echo "${words[1,-2]} ${words[-1]}'" | xargs echo 2>/dev/null > /dev/null; then
completion_input="${words[1,-2]} ${words[-1]}'"
else
local lines="$(echo ${words} | sed 's/$/"/' | CARAPACE_COMPLINE="${words}" CARAPACE_ZSH_HASH_DIRS="$(hash -d)" xargs %v _carapace zsh)"
completion_input="${words[1,-2]} ${words[-1]}\\"
fi


local lines
lines="$(echo "${completion_input}" | CARAPACE_COMPLINE="${words}" CARAPACE_ZSH_HASH_DIRS="$(hash -d)" xargs %v _carapace zsh)"

local zstyle message data
IFS=$'\001' read -r -d '' zstyle message data <<<"${lines}"
# shellcheck disable=SC2154
zstyle ":completion:${curcontext}:*" list-colors "${zstyle}"
zstyle ":completion:${curcontext}:*" group-name ''
[ -z "$message" ] || _message -r "${message}"

local block tag displays values displaysArr valuesArr
local block tag suffix displays values
local -A tags
while IFS=$'\002' read -r -d $'\002' block; do
IFS=$'\003' read -r -d '' tag displays values <<<"${block}"
# shellcheck disable=SC2034
IFS=$'\n' read -r -d $'\004' -A displaysArr <<<"${displays}"$'\004'
IFS=$'\n' read -r -d $'\004' -A valuesArr <<<"${values}"$'\004'

[[ ${#valuesArr[@]} -gt 1 ]] && _describe -t "${tag}" "${tag}" displaysArr valuesArr -Q -S ''
IFS=$'\003' read -r -d '' tag suffix displays values <<<"${block}"

local -a displaysArr=("${(f@)displays}")
local -a valuesArr=("${(f@)values}")

local -a describe_args
if [[ -z ${tags[$tag]} ]]; then
describe_args=(-t "${tag}" "${tag}")
tags[$tag]=1
else
describe_args=(-t "${tag}" "")
fi

local separators=" /,.':@="
if [[ "$suffix" == "" ]]; then
_describe "${describe_args[@]}" displaysArr valuesArr -Q -S ' ' -r "${separators}0-9a-zA-Z"
elif [[ "$separators" == *"$suffix"* ]]; then
_describe "${describe_args[@]}" displaysArr valuesArr -Q -S "$suffix" -r ' '
else
_describe "${describe_args[@]}" displaysArr valuesArr -Q -S "$suffix" -r "0-9a-zA-Z"
fi
done <<<"${data}"
}
compquote '' 2>/dev/null && _%v_completion
compdef _%v_completion %v
`, cmd.Name(), cmd.Name(), uid.Executable(), uid.Executable(), uid.Executable(), cmd.Name(), cmd.Name(), cmd.Name())
`, cmd.Name(), cmd.Name(), uid.Executable(), cmd.Name(), cmd.Name(), cmd.Name())
}
Loading