Skip to content

Commit

Permalink
Fix bugs in the completion widgets
Browse files Browse the repository at this point in the history
Fixes #343.
  • Loading branch information
marlonrichert committed Oct 5, 2021
1 parent ec9de3c commit 7b1a81c
Show file tree
Hide file tree
Showing 7 changed files with 117 additions and 83 deletions.
80 changes: 54 additions & 26 deletions .clitest/complete-word.post.md
Original file line number Diff line number Diff line change
@@ -1,58 +1,86 @@
Setup:
```zsh
% zmodload zsh/param/private
% autoload -Uz zmathfunc && zmathfunc
% autoload -Uz $PWD/functions/widget/.autocomplete.complete-word.post
% unset terminfo
% typeset -gA terminfo=() compstate=( old_list shown ) _lastcomp=()
% terminfo[kcbt]=BACKSPACE
% typeset -gA compstate=() _lastcomp=() terminfo=()
% zstyle ':autocomplete:*' add-space 'FOO' 'TAG' 'BAR'
% zstyle ':autocomplete:(|shift-)tab:' widget-style complete-word
% KEYS=$'\t' WIDGET=complete-word terminfo[kcbt]=BACKTAB
% compstate[old_list]=keep compstate[nmatches]=0 _lastcomp[nmatches]=2
%
```

Only `menu-select` widget sets `$MENUSELECT`:
If we have only 1 match, just insert it:
```zsh
% WIDGET=menu-select .autocomplete.complete-word.post
% print -r -- ${(q+)compstate[insert]} ${(q+)compstate[list]} $+MENUSELECT
1 '' 1
% _lastcomp[nmatches]=1
% .autocomplete.complete-word.post
% print -r -- ${(q+)compstate[insert]} $+compstate[list] $+MENUSELECT
1 0 0
% _lastcomp[nmatches]=2
%
```

Only `Shift-Tab` key sets `$compstate[insert]` to `*:0`:
If we have more than 1 match, but there's no old list, then show the list and don't insert:
```zsh
% KEYS=BACKSPACE .autocomplete.complete-word.post
% print -r -- ${(q+)compstate[insert]} ${(q+)compstate[list]} $+MENUSELECT
0 '' 0
% compstate[old_list]= compstate[nmatches]=2
% .autocomplete.complete-word.post
% print -r -- ${(q+)compstate[insert]} $+compstate[list] $+MENUSELECT
'' 1 0
% compstate[old_list]=keep compstate[nmatches]=0
%
```

`add-space` tag in current completion adds space:
`menu-*` widgets set `$compstate[insert]` to `menu:*`:
```zsh
% _comp_tags='LOREM TAG IPSUM' _lastcomp[tags]='OTHER' .autocomplete.complete-word.post
% print -r -- ${(q+)compstate[insert]} ${(q+)compstate[list]} $+MENUSELECT
'1 ' '' 0
% zstyle ':autocomplete:shift-tab:' widget-style reverse-menu-complete
% KEYS=$terminfo[kcbt]
% .autocomplete.complete-word.post
% print -r -- ${(q+)compstate[insert]} $+compstate[list] $+MENUSELECT
menu:0 0 0
% zstyle ':autocomplete:shift-tab:' widget-style complete-word
% KEYS=$'\t'
%
```

`add-space` tag in previous completion adds space, if current completion is not used:
Widgets default to `menu-select`, which sets `$MENUSELECT`, even without old list:
```zsh
% _comp_tags= _lastcomp[tags]='LOREM TAG IPSUM' .autocomplete.complete-word.post
% print -r -- ${(q+)compstate[insert]} ${(q+)compstate[list]} $+MENUSELECT
'1 ' '' 0
% KEYS=OTHER compstate[old_list]= compstate[nmatches]=2
% .autocomplete.complete-word.post
% print -r -- ${(q+)compstate[insert]} $+compstate[list] $+MENUSELECT
menu:1 0 1
% KEYS=$'\t' compstate[old_list]=keep compstate[nmatches]=0
%
```

`add-space` tag in previous completion does NOT add space, if current completion is used:
`Shift-Tab` key sets `$compstate[insert]` to `*0`:
```zsh
% _comp_tags='OTHER' _lastcomp[tags]='TAG' .autocomplete.complete-word.post
% print -r -- ${(q+)compstate[insert]} ${(q+)compstate[list]} $+MENUSELECT
1 '' 0
% KEYS=$terminfo[kcbt]
% .autocomplete.complete-word.post
% print -r -- ${(q+)compstate[insert]} $+compstate[list] $+MENUSELECT
0 0 0
% KEYS=$'\t'
%
```

If list is not yet shown, then insert unambiguous and show list:
If the list is showing and there's an `add-space` tag in the last completion, then add a space:
```zsh
% compstate[old_list]= _comp_tags= _lastcomp[tags]= .autocomplete.complete-word.post
% print -r -- ${(q+)compstate[insert]} ${(q+)compstate[list]} $+MENUSELECT
unambiguous 'list force packed rows' 0
# % functions -T .autocomplete.complete-word.post
% _comp_tags='OTHER' _lastcomp[tags]='LOREM TAG IPSUM'
% .autocomplete.complete-word.post
% print -r -- ${(q+)compstate[insert]} $+compstate[list] $+MENUSELECT
'1 ' 0 0
% compstate[old_list]= _lastcomp[tags]=
%
```

If the list is not showing and there's an `add-space` tag in the new completion, then add a space:
```zsh
% compstate[old_list]= _comp_tags='LOREM TAG IPSUM' _lastcomp[tags]='OTHER'
% .autocomplete.complete-word.post
% print -r -- ${(q+)compstate[insert]} $+compstate[list] $+MENUSELECT
'1 ' 0 0
% compstate[old_list]=keep _comp_tags= _lastcomp[tags]=
%
```
18 changes: 7 additions & 11 deletions functions/widget/.autocomplete.complete-word.completion-widget
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
#!/bin/zsh
unfunction .autocomplete.complete-word.post 2> /dev/null
builtin autoload -Uz .autocomplete.complete-word.post
zmodload -F zsh/terminfo p:terminfo

private key_name=
case $KEYS in
Expand All @@ -10,18 +9,15 @@ esac

local +h curcontext=${curcontext:-${WIDGET}:::}
local +h -a comppostfuncs=( .autocomplete.complete-word.post "$comppostfuncs[@]" )
if [[ $WIDGET == *menu-* && -v _autocomplete__partial_list ]] ||
( ( [[ $key_name == shift-tab ]] ||
builtin zstyle -t ":autocomplete:${key_name}:" insert-unambiguous
) && [[ -n $compstate[old_list] &&
-v _autocomplete__unambiguous && -n $_autocomplete__unambiguous ]] ); then

if [[ -z $compstate[old_list] ]] || [[ $WIDGET == *menu-* && -v _autocomplete__partial_list ]] ||
( builtin zstyle -t ":autocomplete:${key_name}:" insert-unambiguous &&
[[ -n $_autocomplete__unambiguous ]] ); then
_main_complete
elif [[ -n $compstate[old_list] ]]; then
else
compstate[old_list]=keep
[[ $key_name == shift-tab && $_lastcomp[tags] == *all-matches ]] &&
compstate[insert]=all
_main_complete -
else
_main_complete
fi
return 0 # Prevent beeping.
[[ _lastcomp[nmatches] -gt 0 && -n $compstate[insert] ]]
76 changes: 42 additions & 34 deletions functions/widget/.autocomplete.complete-word.post
Original file line number Diff line number Diff line change
@@ -1,49 +1,50 @@
#autoload
builtin autoload -Uz is-at-least

unset MENUSELECT
unset MENUSELECT 'compstate[list]'
compstate[insert]=

private key_name=
case $KEYS in
( $'\t' ) key_name=tab ;;
( $terminfo[kcbt] ) key_name=shift-tab ;;
esac
local key_style=
builtin zstyle -s :autocomplete:${key_name}: widget-style key_style

if [[ ! -v compstate[old_list] || -z $compstate[old_list] ]] ||
(
[[ -v _autocomplete__unambiguous && -n $_autocomplete__unambiguous ]] &&
builtin zstyle -t ":autocomplete:${key_name}:" insert-unambiguous
); then

if ! is-at-least 5.8.1; then
# Work around a crashing bug in Zsh.
# See [zsh-workers 48936](https://www.zsh.org/mla/workers/2021/msg01162.html).
[[ $key_style == *menu-* && -n $key_name ]] &&
compstate[insert]='automenu-'
fi
local widget_style=
builtin zstyle -s :autocomplete:${key_name}: widget-style widget_style ||
widget_style=menu-select

if [[ -n $_autocomplete__unambiguous ]] &&
builtin zstyle -t ":autocomplete:${key_name}:" insert-unambiguous; then
[[ $widget_style == (|*-)menu-* ]] &&
compstate[insert]='automenu-'
compstate[insert]+=unambiguous
compstate[list]='list force packed rows'
unset _autocomplete__unambiguous
return
fi

if [[ $WIDGET == *menu-select ]]; then
# Determine which terminal line we're on (for async completion).
typeset -gHi _autocomplete__buffer_start_line=$((
max( min( _autocomplete__buffer_start_line, LINES - compstate[list_lines] ), BUFFERLINES )
))
typeset -gHi MENUSELECT=0
private -i nmatches=0
if [[ $compstate[old_list] == keep ]]; then
nmatches=$_lastcomp[nmatches]
else
nmatches=$compstate[nmatches]
fi

if ! is-at-least 5.8.1; then
# Work around a crashing bug in Zsh.
# See [zsh-workers 48936](https://www.zsh.org/mla/workers/2021/msg01162.html).
[[ $key_style == *menu-* && -n $key_name ]] &&
compstate[insert]='menu:'
if (( nmatches > 1 )); then
if [[ $widget_style == (|*-)menu-select ]]; then
typeset -gHi MENUSELECT=0
compstate[insert]='menu:'

# Determine which terminal line we're on (for async completion).
typeset -gHi _autocomplete__buffer_start_line=$((
max( min( _autocomplete__buffer_start_line, LINES - compstate[list_lines] ), BUFFERLINES )
))

elif [[ $compstate[old_list] != keep && $_lastcomp[insert] != (|*-)unambiguous ]]; then
compstate[list]='list force'
return
elif [[ $widget_style == (|*-)menu-complete ]]; then
compstate[insert]='menu:'
fi
fi

if [[ $key_name == shift-tab ]]; then
Expand All @@ -52,9 +53,16 @@ else
compstate[insert]+='1'
fi

local -a tags=() match=() mbegin=() mend=()
builtin zstyle -a :autocomplete: add-space tags ||
tags=( executables aliases functions builtins reserved-words commands )

[[ $RBUFFER != [[:space:]]* && -n ${${=${_comp_tags:-$_lastcomp[tags]}}:*tags} ]] &&
compstate[insert]+=' '
if [[ $RBUFFER != [[:space:]]* ]]; then
local -a spacetags=()
builtin zstyle -a :autocomplete: add-space spacetags ||
spacetags=( executables aliases functions builtins reserved-words commands )
private -a comptags=()
if [[ $compstate[old_list] == keep ]]; then
comptags=( $=_lastcomp[tags] )
else
comptags=( $=_comp_tags )
fi
[[ -n ${comptags:*spacetags} ]] &&
compstate[insert]+=' '
fi
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ $0() {
_main_complete _autocomplete.history_lines

unset curcontext
(( compstate[nmatches] > 0 ))
(( _lastcomp[nmatches] ))
}

$0.post() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ $0() {
compstate[old_list]=
local +h -a comppostfuncs=( $0.post "$comppostfuncs[@]" )
_main_complete
(( _lastcomp[nmatches] ))
}

$0.post() {
Expand Down
19 changes: 10 additions & 9 deletions scripts/.autocomplete.async
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,11 @@ builtin zle -N history-incremental-search-forward .autocomplete.async.history-in

.autocomplete.async.start.inner() {
{
local -F min_delay=
builtin zstyle -s :autocomplete: min-delay min_delay ||
min_delay=0.01
zselect -t "$(( [#10] 100 * max( 0, min_delay - SECONDS ) ))"

private hooks=( chpwd periodic precmd preexec zshaddhistory zshexit )
builtin unset ${^hooks}_functions &> /dev/null
$hooks[@] () { : }
Expand All @@ -224,11 +229,6 @@ builtin zle -N history-incremental-search-forward .autocomplete.async.history-in

zpty -w AUTOCOMPLETE $'\t'

local -F min_delay=
builtin zstyle -s :autocomplete: min-delay min_delay ||
min_delay=0.01
zselect -t "$(( [#10] 100 * max( 0, min_delay - SECONDS ) ))"

local header=
zpty -r AUTOCOMPLETE header $'*\C-B'

Expand Down Expand Up @@ -279,7 +279,8 @@ log_functions+=( .autocomplete.async.pty )
setopt $_autocomplete__ctxt_opts[@]
builtin zle .autocomplete.async.pty.completion-widget -w 2> /dev/null
} always {
print -n -- '\C-C'
print -rNC1 -- \
"$_autocomplete__list_lines" "$_autocomplete__mesg" "$_autocomplete__comp_mesg[@]" '\C-C'
builtin kill $sysparams[pid]
}
} 2>>| $_autocomplete__log_file
Expand All @@ -291,11 +292,11 @@ log_functions+=( .autocomplete.async.pty.zle-widget.inner )

.autocomplete.async.pty.completion-widget.inner() {
if .autocomplete.async.insufficient-input; then
print -rNC1 -- "0" "" ""
typeset -gHi _autocomplete__list_lines=0
return
fi
if .autocomplete.async.same-state; then
print -rNC1 -- "$_lastcomp[list_lines]" "$_autocomplete__mesg" "$_autocomplete__comp_mesg[@]"
typeset -gHi _autocomplete__list_lines=$_lastcomp[list_lines]
return
fi
unset _autocomplete__mesg _autocomplete__comp_mesg
Expand All @@ -320,7 +321,7 @@ log_functions+=( .autocomplete.async.pty.zle-widget.inner )
local +h -a comppostfuncs=( .autocomplete.async.pty.message )
_main_complete
} always {
print -rNC1 -- "$compstate[list_lines]" "$_autocomplete__mesg" "$_autocomplete__comp_mesg[@]"
typeset -gHi _autocomplete__list_lines=$compstate[list_lines]
}
} 2>>| $_autocomplete__log_file
log_functions+=( .autocomplete.async.pty.completion-widget.inner )
Expand Down
4 changes: 2 additions & 2 deletions zsh-autocomplete.plugin.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
.zinit-tmp-subst-off "${___mode:-load}"

zmodload zsh/param/private
setopt NO_flowcontrol NO_singlelinezle
setopt NO_flowcontrol NO_listbeep NO_singlelinezle

() {
emulate -L zsh
Expand All @@ -21,7 +21,7 @@ setopt NO_flowcontrol NO_singlelinezle
)
setopt $_autocomplete__func_opts[@]

typeset -gHa _autocomplete__comp_opts=( localoptions NO_banghist NO_completeinword NO_listbeep )
typeset -gHa _autocomplete__comp_opts=( localoptions NO_banghist NO_completeinword )
typeset -gHa _autocomplete__ctxt_opts=( completealiases completeinword )

private basedir=${${(%):-%x}:P:h}
Expand Down

2 comments on commit 7b1a81c

@mmhj
Copy link

@mmhj mmhj commented on 7b1a81c Oct 15, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi Marlon,

autocomplete on my m1 macbook breaks when I update to this commit or beyond.
I have to revert to an older commit to get the widget back.

For example, when I try to cd to a dir I get this:

missing `external command', `builtin command', `shell function', `alias', `reserved word', `suffix alias', `job', or `parameter'

I'm not sure how to continue from here, but I've managed to pinpoint the commit where it breaks.

@marlonrichert
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mmhj Please open a bug report.

Please sign in to comment.