From c892c1206828a8ff19a0213a14dd2acecbf9dcdd Mon Sep 17 00:00:00 2001 From: maxlandon Date: Fri, 1 Aug 2025 07:11:16 +0200 Subject: [PATCH 1/7] Implement advanced suffix management for ZSH --- internal/shell/zsh/action.go | 32 +++++++++++++---- internal/shell/zsh/snippet.go | 66 ++++++++++++++++++++++++++++------- 2 files changed, 79 insertions(+), 19 deletions(-) diff --git a/internal/shell/zsh/action.go b/internal/shell/zsh/action.go index 0247c8aaa..35085ba75 100644 --- a/internal/shell/zsh/action.go +++ b/internal/shell/zsh/action.go @@ -113,8 +113,21 @@ func ActionRawValues(currentWord string, meta common.Meta, values common.RawValu values.EachTag(func(tag string, values common.RawValues) { vals := make([]string, len(values)) displays := make([]string, len(values)) + suffixes := make([]string, len(values)) for index, val := range values { value := sanitizer.Replace(val.Value) + suffix := "" + + // If a value is set to not have a space suffix, we want to delegate suffix + // management to Zsh's completion system as much as possible. + // The last character of the value is checked to see if it is a nospace + // character itself (like '/' or ','), and if so, it's treated as a suffix + // to be handled by the Zsh script. + if meta.Nospace.Matches(val.Value) && len(val.Value) > 0 { + if meta.Nospace.Matches(lastChar) { + suffix = lastChar + value = strings.TrimSuffix(value, suffix) + } switch state { case QUOTING_ESCAPING_STATE: @@ -136,11 +149,17 @@ func ActionRawValues(currentWord string, meta common.Meta, values common.RawValu value = describeReplacer.Replace(value) } - if !meta.Nospace.Matches(val.Value) { - switch state { - case FULL_QUOTING_ESCAPING_STATE, FULL_QUOTING_STATE: // nospace workaround - default: - value += " " + // If there is no suffix, and the value is not a nospace value, we want to + // add a space suffix. This is also delegated to the Zsh script, which will + // handle the space suffix appropriately (e.g., removing it when another + // space is typed). + if suffix == "" { + if !meta.Nospace.Matches(val.Value) { + switch state { + case FULL_QUOTING_ESCAPING_STATE, FULL_QUOTING_STATE: // nospace workaround + default: + suffix = " " + } } } @@ -149,6 +168,7 @@ func ActionRawValues(currentWord string, meta common.Meta, values common.RawValu description := sanitizer.Replace(val.Description) vals[index] = value + suffixes[index] = suffix if strings.TrimSpace(description) == "" { displays[index] = display @@ -156,7 +176,7 @@ func ActionRawValues(currentWord string, meta common.Meta, values common.RawValu displays[index] = fmt.Sprintf("%v:%v", display, description) } } - tagGroup = append(tagGroup, strings.Join([]string{tag, strings.Join(displays, "\n"), strings.Join(vals, "\n")}, "\003")) + tagGroup = append(tagGroup, strings.Join([]string{tag, strings.Join(displays, "\n"), strings.Join(vals, "\n"), strings.Join(suffixes, "\n")}, "\003")) }) return fmt.Sprintf("%v\001%v\001%v\001", zstyles{values}.Format(), message{meta}.Format(), strings.Join(tagGroup, "\002")+"\002") } diff --git a/internal/shell/zsh/snippet.go b/internal/shell/zsh/snippet.go index 7295151ee..2c96073bc 100644 --- a/internal/shell/zsh/snippet.go +++ b/internal/shell/zsh/snippet.go @@ -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 @@ -16,14 +17,18 @@ function _%v_completion { 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 @@ -31,17 +36,52 @@ function _%v_completion { zstyle ":completion:${curcontext}:*" group-name '' [ -z "$message" ] || _message -r "${message}" - local block tag displays values displaysArr valuesArr + local block tag displays values suffixes 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 displays values suffixes <<<"${block}" + + local -a displaysArr=("${(f@)displays}") + local -a valuesArr=("${(f@)values}") + local -a suffixesArr=("${(f@)suffixes}") + + typeset -A grouped_values=() + typeset -A grouped_displays=() + + for i in {1..${#valuesArr[@]}}; do + local suffix_key="${suffixesArr[$i]:-__NOSUFFIX__}" + grouped_values[$suffix_key]+="${valuesArr[$i]}"$'\n' + grouped_displays[$suffix_key]+="${displaysArr[$i]}"$'\n' + done + + local first_call=1 + for suffix_key in "${(@k)grouped_values}"; do + local -a s_values=("${(f@)${grouped_values[$suffix_key]%%$'\n'}}") + local -a s_displays=("${(f@)${grouped_displays[$suffix_key]%%$'\n'}}") + + if [[ ${#s_values[@]} -eq 0 ]]; then + continue + fi + + local -a describe_args + if (( first_call )); then + describe_args=(-t "${tag}" "${tag}") + first_call=0 + else + describe_args=(-t "${tag}" "") + fi + + local separators=" /,.':@" + if [[ "$suffix_key" == "__NOSUFFIX__" ]]; then + _describe "${describe_args[@]}" s_displays s_values -Q + elif [[ "$separators" == *"$suffix_key"* ]]; then + _describe "${describe_args[@]}" s_displays s_values -Q -S "$suffix_key" -r ' ' + else + _describe "${describe_args[@]}" s_displays s_values -Q -S "$suffix_key" -r "0-9a-zA-Z" + fi + done 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()) } From 7bacdde21566e586a912f91ac36c584ab698512a Mon Sep 17 00:00:00 2001 From: maxlandon Date: Fri, 1 Aug 2025 07:55:12 +0200 Subject: [PATCH 2/7] Fix syntax error --- internal/shell/zsh/action.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/shell/zsh/action.go b/internal/shell/zsh/action.go index 35085ba75..642f63330 100644 --- a/internal/shell/zsh/action.go +++ b/internal/shell/zsh/action.go @@ -124,9 +124,9 @@ func ActionRawValues(currentWord string, meta common.Meta, values common.RawValu // character itself (like '/' or ','), and if so, it's treated as a suffix // to be handled by the Zsh script. if meta.Nospace.Matches(val.Value) && len(val.Value) > 0 { - if meta.Nospace.Matches(lastChar) { - suffix = lastChar - value = strings.TrimSuffix(value, suffix) + lastChar := val.Value[len(val.Value)-1:] + suffix = lastChar + value = strings.TrimSuffix(value, suffix) } switch state { From 2ded73e2bd6139d66f042764a5453e9ad63b6f3f Mon Sep 17 00:00:00 2001 From: maxlandon Date: Fri, 1 Aug 2025 08:19:06 +0200 Subject: [PATCH 3/7] Update ZSH test --- example/cmd/_test/zsh.sh | 62 ++++++++++++++++++++++++++++++++-------- 1 file changed, 50 insertions(+), 12 deletions(-) diff --git a/example/cmd/_test/zsh.sh b/example/cmd/_test/zsh.sh index d95f9346d..a778f7ea1 100644 --- a/example/cmd/_test/zsh.sh +++ b/example/cmd/_test/zsh.sh @@ -4,14 +4,18 @@ function _example_completion { 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 @@ -19,16 +23,50 @@ function _example_completion { zstyle ":completion:${curcontext}:*" group-name '' [ -z "$message" ] || _message -r "${message}" - local block tag displays values displaysArr valuesArr + local block tag displays values suffixes 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 displays values suffixes <<<"${block}" + + local -a displaysArr=("${(f@)displays}") + local -a valuesArr=("${(f@)values}") + local -a suffixesArr=("${(f@)suffixes}") + + typeset -A grouped_values=() + typeset -A grouped_displays=() + + for i in {1..${#valuesArr[@]}}; do + local suffix_key="${suffixesArr[$i]:-__NOSUFFIX__}" + grouped_values[$suffix_key]+="${valuesArr[$i]}"$'\n' + grouped_displays[$suffix_key]+="${displaysArr[$i]}"$'\n' + done + + local first_call=1 + for suffix_key in "${(@k)grouped_values}"; do + local -a s_values=("${(f@)${grouped_values[$suffix_key]%$'\n'}}") + local -a s_displays=("${(f@)${grouped_displays[$suffix_key]%$'\n'}}") + + if [[ ${#s_values[@]} -eq 0 ]]; then + continue + fi + + local -a describe_args + if (( first_call )); then + describe_args=(-t "${tag}" "${tag}") + first_call=0 + else + describe_args=(-t "${tag}" "") + fi + + local separators=" /,.':@" + if [[ "$suffix_key" == "__NOSUFFIX__" ]]; then + _describe "${describe_args[@]}" s_displays s_values -Q + elif [[ "$separators" == *"$suffix_key"* ]]; then + _describe "${describe_args[@]}" s_displays s_values -Q -S "$suffix_key" -r ' ' + else + _describe "${describe_args[@]}" s_displays s_values -Q -S "$suffix_key" -r "0-9a-zA-Z" + fi + done done <<<"${data}" } compquote '' 2>/dev/null && _example_completion compdef _example_completion example - From 7596fc28159d8cd629a94b71af73e534ab1237f1 Mon Sep 17 00:00:00 2001 From: maxlandon Date: Fri, 1 Aug 2025 08:22:00 +0200 Subject: [PATCH 4/7] Fix space in ZSH test script --- example/cmd/_test/zsh.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/example/cmd/_test/zsh.sh b/example/cmd/_test/zsh.sh index a778f7ea1..e186f5e02 100644 --- a/example/cmd/_test/zsh.sh +++ b/example/cmd/_test/zsh.sh @@ -70,3 +70,4 @@ function _example_completion { } compquote '' 2>/dev/null && _example_completion compdef _example_completion example + From bb95841cec8bd4d47e48070949c9eba874a79f5e Mon Sep 17 00:00:00 2001 From: maxlandon Date: Sat, 2 Aug 2025 21:28:40 +0200 Subject: [PATCH 5/7] Change the logic for grouping by suffix --- internal/common/suffix.go | 6 +- internal/shell/zsh/action.go | 157 ++++++++++++++++++++-------------- internal/shell/zsh/snippet.go | 68 ++++++--------- 3 files changed, 123 insertions(+), 108 deletions(-) diff --git a/internal/common/suffix.go b/internal/common/suffix.go index 615f820e9..661dc8871 100644 --- a/internal/common/suffix.go +++ b/internal/common/suffix.go @@ -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] } diff --git a/internal/shell/zsh/action.go b/internal/shell/zsh/action.go index 642f63330..e10712b96 100644 --- a/internal/shell/zsh/action.go +++ b/internal/shell/zsh/action.go @@ -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( @@ -17,7 +20,7 @@ var sanitizer = strings.NewReplacer( ) var quotingReplacer = strings.NewReplacer( - `'`, `'\''`, + `'`, `'''`, ) var quotingEscapingReplacer = strings.NewReplacer( @@ -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 { @@ -100,83 +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)) - suffixes := make([]string, len(values)) - for index, val := range values { - value := sanitizer.Replace(val.Value) - suffix := "" - - // If a value is set to not have a space suffix, we want to delegate suffix - // management to Zsh's completion system as much as possible. - // The last character of the value is checked to see if it is a nospace - // character itself (like '/' or ','), and if so, it's treated as a suffix - // to be handled by the Zsh script. - if meta.Nospace.Matches(val.Value) && len(val.Value) > 0 { - lastChar := val.Value[len(val.Value)-1:] - suffix = lastChar - value = strings.TrimSuffix(value, suffix) - } + 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)) - 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) - } + for index, val := range suffixedValues { + value := sanitizer.Replace(val.Value) + if suffix != " " && suffix != "" { + value = strings.TrimSuffix(value, suffix) + } + + 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) + } + + 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) - // If there is no suffix, and the value is not a nospace value, we want to - // add a space suffix. This is also delegated to the Zsh script, which will - // handle the space suffix appropriately (e.g., removing it when another - // space is typed). - if suffix == "" { - if !meta.Nospace.Matches(val.Value) { - switch state { - case FULL_QUOTING_ESCAPING_STATE, FULL_QUOTING_STATE: // nospace workaround - default: - suffix = " " - } + 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 - suffixes[index] = suffix + // 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"), strings.Join(suffixes, "\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) } diff --git a/internal/shell/zsh/snippet.go b/internal/shell/zsh/snippet.go index 2c96073bc..6795e5fc5 100644 --- a/internal/shell/zsh/snippet.go +++ b/internal/shell/zsh/snippet.go @@ -15,7 +15,7 @@ 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 @@ -25,10 +25,13 @@ function _%v_completion { else completion_input="${words[1,-2]} ${words[-1]}\\" fi - + + local go_start + go_start=$(zsh_timer) + 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 @@ -36,49 +39,30 @@ function _%v_completion { zstyle ":completion:${curcontext}:*" group-name '' [ -z "$message" ] || _message -r "${message}" - local block tag displays values suffixes + 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 suffixes <<<"${block}" - + IFS=$'\003' read -r -d '' tag suffix displays values <<<"${block}" + local -a displaysArr=("${(f@)displays}") local -a valuesArr=("${(f@)values}") - local -a suffixesArr=("${(f@)suffixes}") - - typeset -A grouped_values=() - typeset -A grouped_displays=() - - for i in {1..${#valuesArr[@]}}; do - local suffix_key="${suffixesArr[$i]:-__NOSUFFIX__}" - grouped_values[$suffix_key]+="${valuesArr[$i]}"$'\n' - grouped_displays[$suffix_key]+="${displaysArr[$i]}"$'\n' - done - - local first_call=1 - for suffix_key in "${(@k)grouped_values}"; do - local -a s_values=("${(f@)${grouped_values[$suffix_key]%%$'\n'}}") - local -a s_displays=("${(f@)${grouped_displays[$suffix_key]%%$'\n'}}") - - if [[ ${#s_values[@]} -eq 0 ]]; then - continue - fi - - local -a describe_args - if (( first_call )); then - describe_args=(-t "${tag}" "${tag}") - first_call=0 - else - describe_args=(-t "${tag}" "") - fi - local separators=" /,.':@" - if [[ "$suffix_key" == "__NOSUFFIX__" ]]; then - _describe "${describe_args[@]}" s_displays s_values -Q - elif [[ "$separators" == *"$suffix_key"* ]]; then - _describe "${describe_args[@]}" s_displays s_values -Q -S "$suffix_key" -r ' ' - else - _describe "${describe_args[@]}" s_displays s_values -Q -S "$suffix_key" -r "0-9a-zA-Z" - fi - done + 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 From 2b520ceba0ade7bf5cbd9026b43575de16bb7f16 Mon Sep 17 00:00:00 2001 From: maxlandon Date: Sat, 2 Aug 2025 21:45:01 +0200 Subject: [PATCH 6/7] Remove debug code --- internal/shell/zsh/snippet.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/internal/shell/zsh/snippet.go b/internal/shell/zsh/snippet.go index 6795e5fc5..286045820 100644 --- a/internal/shell/zsh/snippet.go +++ b/internal/shell/zsh/snippet.go @@ -26,9 +26,6 @@ function _%v_completion { completion_input="${words[1,-2]} ${words[-1]}\\" fi - local go_start - go_start=$(zsh_timer) - local lines lines="$(echo "${completion_input}" | CARAPACE_COMPLINE="${words}" CARAPACE_ZSH_HASH_DIRS="$(hash -d)" xargs %v _carapace zsh)" From e1fb909ef121a4b6b692f11304fb1bc38407c899 Mon Sep 17 00:00:00 2001 From: maxlandon Date: Sat, 2 Aug 2025 21:48:10 +0200 Subject: [PATCH 7/7] Update ZSH script in tests --- example/cmd/_test/zsh.sh | 65 ++++++++++++++-------------------------- 1 file changed, 23 insertions(+), 42 deletions(-) diff --git a/example/cmd/_test/zsh.sh b/example/cmd/_test/zsh.sh index e186f5e02..0b6672108 100644 --- a/example/cmd/_test/zsh.sh +++ b/example/cmd/_test/zsh.sh @@ -2,7 +2,7 @@ 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 @@ -12,10 +12,10 @@ function _example_completion { else 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 @@ -23,49 +23,30 @@ function _example_completion { zstyle ":completion:${curcontext}:*" group-name '' [ -z "$message" ] || _message -r "${message}" - local block tag displays values suffixes + 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 suffixes <<<"${block}" - + IFS=$'\003' read -r -d '' tag suffix displays values <<<"${block}" + local -a displaysArr=("${(f@)displays}") local -a valuesArr=("${(f@)values}") - local -a suffixesArr=("${(f@)suffixes}") - - typeset -A grouped_values=() - typeset -A grouped_displays=() - - for i in {1..${#valuesArr[@]}}; do - local suffix_key="${suffixesArr[$i]:-__NOSUFFIX__}" - grouped_values[$suffix_key]+="${valuesArr[$i]}"$'\n' - grouped_displays[$suffix_key]+="${displaysArr[$i]}"$'\n' - done - - local first_call=1 - for suffix_key in "${(@k)grouped_values}"; do - local -a s_values=("${(f@)${grouped_values[$suffix_key]%$'\n'}}") - local -a s_displays=("${(f@)${grouped_displays[$suffix_key]%$'\n'}}") - - if [[ ${#s_values[@]} -eq 0 ]]; then - continue - fi - - local -a describe_args - if (( first_call )); then - describe_args=(-t "${tag}" "${tag}") - first_call=0 - else - describe_args=(-t "${tag}" "") - fi - local separators=" /,.':@" - if [[ "$suffix_key" == "__NOSUFFIX__" ]]; then - _describe "${describe_args[@]}" s_displays s_values -Q - elif [[ "$separators" == *"$suffix_key"* ]]; then - _describe "${describe_args[@]}" s_displays s_values -Q -S "$suffix_key" -r ' ' - else - _describe "${describe_args[@]}" s_displays s_values -Q -S "$suffix_key" -r "0-9a-zA-Z" - fi - done + 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