From 82b08e2dc8c59a40d363cb0c12f3516937cb1733 Mon Sep 17 00:00:00 2001 From: Eric Freese Date: Wed, 16 May 2018 15:27:06 -0600 Subject: [PATCH 01/15] First pass at getting suggestions from completion engine (#111) Uses https://github.com/Valodim/zsh-capture-completion with some slight modifications. --- src/config.zsh | 5 +- src/strategies/completion.zsh | 132 ++++++++++++++++++++++++++++++++ zsh-autosuggestions.zsh | 137 +++++++++++++++++++++++++++++++++- 3 files changed, 272 insertions(+), 2 deletions(-) create mode 100644 src/strategies/completion.zsh diff --git a/src/config.zsh b/src/config.zsh index 597307f..9b3dbae 100644 --- a/src/config.zsh +++ b/src/config.zsh @@ -68,4 +68,7 @@ ZSH_AUTOSUGGEST_IGNORE_WIDGETS=( ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE= # Pty name for calculating autosuggestions asynchronously -ZSH_AUTOSUGGEST_ASYNC_PTY_NAME=zsh_autosuggest_pty +ZSH_AUTOSUGGEST_ASYNC_PTY_NAME=zsh_autosuggest_async_pty + +# Pty name for capturing completions for completion suggestion strategy +ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME=zsh_autosuggest_completion_pty diff --git a/src/strategies/completion.zsh b/src/strategies/completion.zsh new file mode 100644 index 0000000..db948a0 --- /dev/null +++ b/src/strategies/completion.zsh @@ -0,0 +1,132 @@ + +#--------------------------------------------------------------------# +# Completion Suggestion Strategy # +#--------------------------------------------------------------------# +# Fetches suggestions from zsh's completion engine +# + +# Big thanks to https://github.com/Valodim/zsh-capture-completion +_zsh_autosuggest_capture_completion() { + zpty $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME zsh -f -i + + # line buffer for pty output + local line + + setopt rcquotes + () { + zpty -w $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME source $1 + repeat 4; do + zpty -r $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME line + [[ $line == ok* ]] && return + done + echo 'error initializing.' >&2 + exit 2 + } =( <<< ' + # no prompt! + PROMPT= + # load completion system + autoload compinit + compinit -d ~/.zcompdump_autosuggestions + # never run a command + bindkey ''^M'' undefined + bindkey ''^J'' undefined + bindkey ''^I'' complete-word + # send a line with null-byte at the end before and after completions are output + null-line () { + echo -E - $''\0'' + } + compprefuncs=( null-line ) + comppostfuncs=( null-line exit ) + # never group stuff! + zstyle '':completion:*'' list-grouped false + # don''t insert tab when attempting completion on empty line + zstyle '':completion:*'' insert-tab false + # no list separator, this saves some stripping later on + zstyle '':completion:*'' list-separator '''' + # we use zparseopts + zmodload zsh/zutil + # override compadd (this our hook) + compadd () { + # check if any of -O, -A or -D are given + if [[ ${@[1,(i)(-|--)]} == *-(O|A|D)\ * ]]; then + # if that is the case, just delegate and leave + builtin compadd "$@" + return $? + fi + # ok, this concerns us! + # echo -E - got this: "$@" + # be careful with namespacing here, we don''t want to mess with stuff that + # should be passed to compadd! + typeset -a __hits __dscr __tmp + # do we have a description parameter? + # note we don''t use zparseopts here because of combined option parameters + # with arguments like -default- confuse it. + if (( $@[(I)-d] )); then # kind of a hack, $+@[(r)-d] doesn''t work because of line noise overload + # next param after -d + __tmp=${@[$[${@[(i)-d]}+1]]} + # description can be given as an array parameter name, or inline () array + if [[ $__tmp == \(* ]]; then + eval "__dscr=$__tmp" + else + __dscr=( "${(@P)__tmp}" ) + fi + fi + # capture completions by injecting -A parameter into the compadd call. + # this takes care of matching for us. + builtin compadd -A __hits -D __dscr "$@" + # JESUS CHRIST IT TOOK ME FOREVER TO FIGURE OUT THIS OPTION WAS SET AND WAS MESSING WITH MY SHIT HERE + setopt localoptions norcexpandparam extendedglob + # extract prefixes and suffixes from compadd call. we can''t do zsh''s cool + # -r remove-func magic, but it''s better than nothing. + typeset -A apre hpre hsuf asuf + zparseopts -E P:=apre p:=hpre S:=asuf s:=hsuf + # append / to directories? we are only emulating -f in a half-assed way + # here, but it''s better than nothing. + integer dirsuf=0 + # don''t be fooled by -default- >.> + if [[ -z $hsuf && "${${@//-default-/}% -# *}" == *-[[:alnum:]]#f* ]]; then + dirsuf=1 + fi + # just drop + [[ -n $__hits ]] || return + # this is the point where we have all matches in $__hits and all + # descriptions in $__dscr! + # display all matches + local dsuf dscr + for i in {1..$#__hits}; do + # add a dir suffix? + (( dirsuf )) && [[ -d $__hits[$i] ]] && dsuf=/ || dsuf= + # description to be displayed afterwards + (( $#__dscr >= $i )) && dscr=" -- ${${__dscr[$i]}##$__hits[$i] #}" || dscr= + echo -E - $IPREFIX$apre$hpre$__hits[$i]$dsuf$hsuf$asuf + done + } + # signal success! + echo ok') + + zpty -w $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME "$*"$'\t' + + integer tog=0 + # read from the pty, and parse linewise + while zpty -r $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME; do :; done | while IFS= read -r line; do + if [[ $line == *$'\0\r' ]]; then + (( tog++ )) && return 0 || continue + fi + # display between toggles + (( tog )) && echo -E - $line + done + + return 2 +} + +_zsh_autosuggest_strategy_completion() { + typeset -g suggestion=$(_zsh_autosuggest_capture_completion "$1" | head -n 1) + + # Strip the trailing carriage return + suggestion="${suggestion%$'\r'}" + + # Add the completion string to the buffer to build the full suggestion + local -i i=1 + while [[ "$suggestion" != "${1[$i,-1]}"* ]]; do ((i++)); done + suggestion="${1[1,$i-1]}$suggestion" +} diff --git a/zsh-autosuggestions.zsh b/zsh-autosuggestions.zsh index e2e06be..edd6d84 100644 --- a/zsh-autosuggestions.zsh +++ b/zsh-autosuggestions.zsh @@ -104,7 +104,10 @@ ZSH_AUTOSUGGEST_IGNORE_WIDGETS=( ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE= # Pty name for calculating autosuggestions asynchronously -ZSH_AUTOSUGGEST_ASYNC_PTY_NAME=zsh_autosuggest_pty +ZSH_AUTOSUGGEST_ASYNC_PTY_NAME=zsh_autosuggest_async_pty + +# Pty name for capturing completions for completion suggestion strategy +ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME=zsh_autosuggest_completion_pty #--------------------------------------------------------------------# # Utility Functions # @@ -493,6 +496,138 @@ zle -N autosuggest-enable _zsh_autosuggest_widget_enable zle -N autosuggest-disable _zsh_autosuggest_widget_disable zle -N autosuggest-toggle _zsh_autosuggest_widget_toggle +#--------------------------------------------------------------------# +# Completion Suggestion Strategy # +#--------------------------------------------------------------------# +# Fetches suggestions from zsh's completion engine +# + +# Big thanks to https://github.com/Valodim/zsh-capture-completion +_zsh_autosuggest_capture_completion() { + zpty $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME zsh -f -i + + # line buffer for pty output + local line + + setopt rcquotes + () { + zpty -w $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME source $1 + repeat 4; do + zpty -r $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME line + [[ $line == ok* ]] && return + done + echo 'error initializing.' >&2 + exit 2 + } =( <<< ' + # no prompt! + PROMPT= + # load completion system + autoload compinit + compinit -d ~/.zcompdump_autosuggestions + # never run a command + bindkey ''^M'' undefined + bindkey ''^J'' undefined + bindkey ''^I'' complete-word + # send a line with null-byte at the end before and after completions are output + null-line () { + echo -E - $''\0'' + } + compprefuncs=( null-line ) + comppostfuncs=( null-line exit ) + # never group stuff! + zstyle '':completion:*'' list-grouped false + # don''t insert tab when attempting completion on empty line + zstyle '':completion:*'' insert-tab false + # no list separator, this saves some stripping later on + zstyle '':completion:*'' list-separator '''' + # we use zparseopts + zmodload zsh/zutil + # override compadd (this our hook) + compadd () { + # check if any of -O, -A or -D are given + if [[ ${@[1,(i)(-|--)]} == *-(O|A|D)\ * ]]; then + # if that is the case, just delegate and leave + builtin compadd "$@" + return $? + fi + # ok, this concerns us! + # echo -E - got this: "$@" + # be careful with namespacing here, we don''t want to mess with stuff that + # should be passed to compadd! + typeset -a __hits __dscr __tmp + # do we have a description parameter? + # note we don''t use zparseopts here because of combined option parameters + # with arguments like -default- confuse it. + if (( $@[(I)-d] )); then # kind of a hack, $+@[(r)-d] doesn''t work because of line noise overload + # next param after -d + __tmp=${@[$[${@[(i)-d]}+1]]} + # description can be given as an array parameter name, or inline () array + if [[ $__tmp == \(* ]]; then + eval "__dscr=$__tmp" + else + __dscr=( "${(@P)__tmp}" ) + fi + fi + # capture completions by injecting -A parameter into the compadd call. + # this takes care of matching for us. + builtin compadd -A __hits -D __dscr "$@" + # JESUS CHRIST IT TOOK ME FOREVER TO FIGURE OUT THIS OPTION WAS SET AND WAS MESSING WITH MY SHIT HERE + setopt localoptions norcexpandparam extendedglob + # extract prefixes and suffixes from compadd call. we can''t do zsh''s cool + # -r remove-func magic, but it''s better than nothing. + typeset -A apre hpre hsuf asuf + zparseopts -E P:=apre p:=hpre S:=asuf s:=hsuf + # append / to directories? we are only emulating -f in a half-assed way + # here, but it''s better than nothing. + integer dirsuf=0 + # don''t be fooled by -default- >.> + if [[ -z $hsuf && "${${@//-default-/}% -# *}" == *-[[:alnum:]]#f* ]]; then + dirsuf=1 + fi + # just drop + [[ -n $__hits ]] || return + # this is the point where we have all matches in $__hits and all + # descriptions in $__dscr! + # display all matches + local dsuf dscr + for i in {1..$#__hits}; do + # add a dir suffix? + (( dirsuf )) && [[ -d $__hits[$i] ]] && dsuf=/ || dsuf= + # description to be displayed afterwards + (( $#__dscr >= $i )) && dscr=" -- ${${__dscr[$i]}##$__hits[$i] #}" || dscr= + echo -E - $IPREFIX$apre$hpre$__hits[$i]$dsuf$hsuf$asuf + done + } + # signal success! + echo ok') + + zpty -w $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME "$*"$'\t' + + integer tog=0 + # read from the pty, and parse linewise + while zpty -r $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME; do :; done | while IFS= read -r line; do + if [[ $line == *$'\0\r' ]]; then + (( tog++ )) && return 0 || continue + fi + # display between toggles + (( tog )) && echo -E - $line + done + + return 2 +} + +_zsh_autosuggest_strategy_completion() { + typeset -g suggestion=$(_zsh_autosuggest_capture_completion "$1" | head -n 1) + + # Strip the trailing carriage return + suggestion="${suggestion%$'\r'}" + + # Add the completion string to the buffer to build the full suggestion + local -i i=1 + while [[ "$suggestion" != "${1[$i,-1]}"* ]]; do ((i++)); done + suggestion="${1[1,$i-1]}$suggestion" +} + #--------------------------------------------------------------------# # Default Suggestion Strategy # #--------------------------------------------------------------------# From c5551daabcea48d11028036df88a43a064934d18 Mon Sep 17 00:00:00 2001 From: Eric Freese Date: Wed, 16 May 2018 15:52:46 -0600 Subject: [PATCH 02/15] Default strategy now tries history first and falls back to completion --- src/strategies/default.zsh | 22 +++++++--------------- src/strategies/history.zsh | 25 +++++++++++++++++++++++++ zsh-autosuggestions.zsh | 19 ++++++++++++++++++- 3 files changed, 50 insertions(+), 16 deletions(-) create mode 100644 src/strategies/history.zsh diff --git a/src/strategies/default.zsh b/src/strategies/default.zsh index 0e85fb5..68617ff 100644 --- a/src/strategies/default.zsh +++ b/src/strategies/default.zsh @@ -2,24 +2,16 @@ #--------------------------------------------------------------------# # Default Suggestion Strategy # #--------------------------------------------------------------------# -# Suggests the most recent history item that matches the given -# prefix. +# Will provide suggestions from your history. If no matches are found +# in history, will provide a suggestion from the completion engine. # _zsh_autosuggest_strategy_default() { - # Reset options to defaults and enable LOCAL_OPTIONS - emulate -L zsh + typeset -g suggestion - # Enable globbing flags so that we can use (#m) - setopt EXTENDED_GLOB + _zsh_autosuggest_strategy_history "$1" - # Escape backslashes and all of the glob operators so we can use - # this string as a pattern to search the $history associative array. - # - (#m) globbing flag enables setting references for match data - # TODO: Use (b) flag when we can drop support for zsh older than v5.0.8 - local prefix="${1//(#m)[\\*?[\]<>()|^~#]/\\$MATCH}" - - # Get the history items that match - # - (r) subscript flag makes the pattern match on values - typeset -g suggestion="${history[(r)${prefix}*]}" + if [[ -z "$suggestion" ]]; then + _zsh_autosuggest_strategy_completion "$1" + fi } diff --git a/src/strategies/history.zsh b/src/strategies/history.zsh new file mode 100644 index 0000000..a2755a5 --- /dev/null +++ b/src/strategies/history.zsh @@ -0,0 +1,25 @@ + +#--------------------------------------------------------------------# +# History Suggestion Strategy # +#--------------------------------------------------------------------# +# Suggests the most recent history item that matches the given +# prefix. +# + +_zsh_autosuggest_strategy_history() { + # Reset options to defaults and enable LOCAL_OPTIONS + emulate -L zsh + + # Enable globbing flags so that we can use (#m) + setopt EXTENDED_GLOB + + # Escape backslashes and all of the glob operators so we can use + # this string as a pattern to search the $history associative array. + # - (#m) globbing flag enables setting references for match data + # TODO: Use (b) flag when we can drop support for zsh older than v5.0.8 + local prefix="${1//(#m)[\\*?[\]<>()|^~#]/\\$MATCH}" + + # Get the history items that match + # - (r) subscript flag makes the pattern match on values + typeset -g suggestion="${history[(r)${prefix}*]}" +} diff --git a/zsh-autosuggestions.zsh b/zsh-autosuggestions.zsh index edd6d84..41336a7 100644 --- a/zsh-autosuggestions.zsh +++ b/zsh-autosuggestions.zsh @@ -631,11 +631,28 @@ _zsh_autosuggest_strategy_completion() { #--------------------------------------------------------------------# # Default Suggestion Strategy # #--------------------------------------------------------------------# +# Will provide suggestions from your history. If no matches are found +# in history, will provide a suggestion from the completion engine. +# + +_zsh_autosuggest_strategy_default() { + typeset -g suggestion + + _zsh_autosuggest_strategy_history "$1" + + if [[ -z "$suggestion" ]]; then + _zsh_autosuggest_strategy_completion "$1" + fi +} + +#--------------------------------------------------------------------# +# History Suggestion Strategy # +#--------------------------------------------------------------------# # Suggests the most recent history item that matches the given # prefix. # -_zsh_autosuggest_strategy_default() { +_zsh_autosuggest_strategy_history() { # Reset options to defaults and enable LOCAL_OPTIONS emulate -L zsh From f63afd5969a7c4fe098f686c005dee8d07b6b058 Mon Sep 17 00:00:00 2001 From: Eric Freese Date: Wed, 16 May 2018 16:29:01 -0600 Subject: [PATCH 03/15] Fix async pty name option spec --- spec/options/async_zpty_name_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/options/async_zpty_name_spec.rb b/spec/options/async_zpty_name_spec.rb index 407ee70..60be875 100644 --- a/spec/options/async_zpty_name_spec.rb +++ b/spec/options/async_zpty_name_spec.rb @@ -3,7 +3,7 @@ context 'when async suggestions are enabled' do describe 'the zpty for async suggestions' do it 'is created with the default name' do - session.run_command('zpty -t zsh_autosuggest_pty &>/dev/null; echo $?') + session.run_command('zpty -t zsh_autosuggest_async_pty &>/dev/null; echo $?') wait_for { session.content }.to end_with("\n0") end From 4cca26ec84e764b077175ae20c54c8d673061fc2 Mon Sep 17 00:00:00 2001 From: Eric Freese Date: Fri, 18 May 2018 15:05:45 -0600 Subject: [PATCH 04/15] Modify completion code to better fit our needs Only need the first completion result --- src/strategies/completion.zsh | 124 ++++++++++++--------------------- zsh-autosuggestions.zsh | 125 ++++++++++++---------------------- 2 files changed, 85 insertions(+), 164 deletions(-) diff --git a/src/strategies/completion.zsh b/src/strategies/completion.zsh index db948a0..aa87673 100644 --- a/src/strategies/completion.zsh +++ b/src/strategies/completion.zsh @@ -9,114 +9,74 @@ _zsh_autosuggest_capture_completion() { zpty $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME zsh -f -i - # line buffer for pty output local line setopt rcquotes () { - zpty -w $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME source $1 - repeat 4; do - zpty -r $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME line - [[ $line == ok* ]] && return - done - echo 'error initializing.' >&2 - exit 2 + # Initialize the pty env, blocking until null byte is seen + zpty -w $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME "source $1" + zpty -r $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME line '*'$'\0' } =( <<< ' - # no prompt! - PROMPT= - # load completion system + exec 2>/dev/null # Silence any error messages + autoload compinit compinit -d ~/.zcompdump_autosuggestions - # never run a command - bindkey ''^M'' undefined - bindkey ''^J'' undefined - bindkey ''^I'' complete-word - # send a line with null-byte at the end before and after completions are output - null-line () { - echo -E - $''\0'' - } - compprefuncs=( null-line ) - comppostfuncs=( null-line exit ) - # never group stuff! + + # Exit as soon as completion is finished + comppostfuncs=( exit ) + + # Never group stuff! zstyle '':completion:*'' list-grouped false - # don''t insert tab when attempting completion on empty line - zstyle '':completion:*'' insert-tab false + # no list separator, this saves some stripping later on zstyle '':completion:*'' list-separator '''' + # we use zparseopts zmodload zsh/zutil + # override compadd (this our hook) compadd () { - # check if any of -O, -A or -D are given + # Just delegate and leave if any of -O, -A or -D are given if [[ ${@[1,(i)(-|--)]} == *-(O|A|D)\ * ]]; then - # if that is the case, just delegate and leave builtin compadd "$@" return $? fi - # ok, this concerns us! - # echo -E - got this: "$@" - # be careful with namespacing here, we don''t want to mess with stuff that - # should be passed to compadd! - typeset -a __hits __dscr __tmp - # do we have a description parameter? - # note we don''t use zparseopts here because of combined option parameters - # with arguments like -default- confuse it. - if (( $@[(I)-d] )); then # kind of a hack, $+@[(r)-d] doesn''t work because of line noise overload - # next param after -d - __tmp=${@[$[${@[(i)-d]}+1]]} - # description can be given as an array parameter name, or inline () array - if [[ $__tmp == \(* ]]; then - eval "__dscr=$__tmp" - else - __dscr=( "${(@P)__tmp}" ) - fi - fi - # capture completions by injecting -A parameter into the compadd call. - # this takes care of matching for us. - builtin compadd -A __hits -D __dscr "$@" - # JESUS CHRIST IT TOOK ME FOREVER TO FIGURE OUT THIS OPTION WAS SET AND WAS MESSING WITH MY SHIT HERE + setopt localoptions norcexpandparam extendedglob - # extract prefixes and suffixes from compadd call. we can''t do zsh''s cool + + typeset -a __hits + + # Capture completions by injecting -A parameter into the compadd call. + # This takes care of matching for us. + builtin compadd -A __hits "$@" + + # Exit if no completion results + [[ -n $__hits ]] || return + + # Extract prefixes and suffixes from compadd call. we can''t do zsh''s cool # -r remove-func magic, but it''s better than nothing. typeset -A apre hpre hsuf asuf zparseopts -E P:=apre p:=hpre S:=asuf s:=hsuf - # append / to directories? we are only emulating -f in a half-assed way - # here, but it''s better than nothing. - integer dirsuf=0 - # don''t be fooled by -default- >.> - if [[ -z $hsuf && "${${@//-default-/}% -# *}" == *-[[:alnum:]]#f* ]]; then - dirsuf=1 - fi - # just drop - [[ -n $__hits ]] || return - # this is the point where we have all matches in $__hits and all - # descriptions in $__dscr! - # display all matches - local dsuf dscr - for i in {1..$#__hits}; do - # add a dir suffix? - (( dirsuf )) && [[ -d $__hits[$i] ]] && dsuf=/ || dsuf= - # description to be displayed afterwards - (( $#__dscr >= $i )) && dscr=" -- ${${__dscr[$i]}##$__hits[$i] #}" || dscr= - echo -E - $IPREFIX$apre$hpre$__hits[$i]$dsuf$hsuf$asuf - done - } - # signal success! - echo ok') + # Print the first match + echo -nE - $''\0''$IPREFIX$apre$hpre$__hits[1]$dsuf$hsuf$asuf$''\0'' + } + + # Signal setup completion by sending null byte + echo $''\0'' + ') + + # Send the string and a tab to trigger completion zpty -w $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME "$*"$'\t' - integer tog=0 - # read from the pty, and parse linewise - while zpty -r $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME; do :; done | while IFS= read -r line; do - if [[ $line == *$'\0\r' ]]; then - (( tog++ )) && return 0 || continue - fi - # display between toggles - (( tog )) && echo -E - $line - done + # Read up to the start of the first result + zpty -r $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME line '*'$'\0' - return 2 + # Read the first result + zpty -r $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME line '*'$'\0' + + # Print it, removing the trailing null byte + echo -E - ${line%$'\0'} } _zsh_autosuggest_strategy_completion() { diff --git a/zsh-autosuggestions.zsh b/zsh-autosuggestions.zsh index 41336a7..a2134da 100644 --- a/zsh-autosuggestions.zsh +++ b/zsh-autosuggestions.zsh @@ -506,114 +506,75 @@ zle -N autosuggest-toggle _zsh_autosuggest_widget_toggle _zsh_autosuggest_capture_completion() { zpty $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME zsh -f -i - # line buffer for pty output local line setopt rcquotes () { - zpty -w $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME source $1 - repeat 4; do - zpty -r $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME line - [[ $line == ok* ]] && return - done - echo 'error initializing.' >&2 - exit 2 + # Setup, blocking until null byte to signal completion + zpty -w $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME "source $1" + zpty -r $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME line '*'$'\0' } =( <<< ' - # no prompt! - PROMPT= - # load completion system + exec 2>/dev/null # Silence any error messages + autoload compinit compinit -d ~/.zcompdump_autosuggestions - # never run a command - bindkey ''^M'' undefined - bindkey ''^J'' undefined - bindkey ''^I'' complete-word - # send a line with null-byte at the end before and after completions are output - null-line () { - echo -E - $''\0'' - } - compprefuncs=( null-line ) - comppostfuncs=( null-line exit ) - # never group stuff! + + # Exit as soon as completion is finished + comppostfuncs=( exit ) + + # Never group stuff! zstyle '':completion:*'' list-grouped false - # don''t insert tab when attempting completion on empty line - zstyle '':completion:*'' insert-tab false + # no list separator, this saves some stripping later on zstyle '':completion:*'' list-separator '''' + # we use zparseopts zmodload zsh/zutil + # override compadd (this our hook) compadd () { - # check if any of -O, -A or -D are given + # Just delegate and leave if any of -O, -A or -D are given if [[ ${@[1,(i)(-|--)]} == *-(O|A|D)\ * ]]; then - # if that is the case, just delegate and leave builtin compadd "$@" return $? fi - # ok, this concerns us! - # echo -E - got this: "$@" - # be careful with namespacing here, we don''t want to mess with stuff that - # should be passed to compadd! - typeset -a __hits __dscr __tmp - # do we have a description parameter? - # note we don''t use zparseopts here because of combined option parameters - # with arguments like -default- confuse it. - if (( $@[(I)-d] )); then # kind of a hack, $+@[(r)-d] doesn''t work because of line noise overload - # next param after -d - __tmp=${@[$[${@[(i)-d]}+1]]} - # description can be given as an array parameter name, or inline () array - if [[ $__tmp == \(* ]]; then - eval "__dscr=$__tmp" - else - __dscr=( "${(@P)__tmp}" ) - fi - fi - # capture completions by injecting -A parameter into the compadd call. - # this takes care of matching for us. - builtin compadd -A __hits -D __dscr "$@" - # JESUS CHRIST IT TOOK ME FOREVER TO FIGURE OUT THIS OPTION WAS SET AND WAS MESSING WITH MY SHIT HERE + setopt localoptions norcexpandparam extendedglob - # extract prefixes and suffixes from compadd call. we can''t do zsh''s cool + + typeset -a __hits + + # Capture completions by injecting -A parameter into the compadd call. + # This takes care of matching for us. + builtin compadd -A __hits "$@" + + # Exit if no completion results + [[ -n $__hits ]] || return + + # Extract prefixes and suffixes from compadd call. we can''t do zsh''s cool # -r remove-func magic, but it''s better than nothing. typeset -A apre hpre hsuf asuf zparseopts -E P:=apre p:=hpre S:=asuf s:=hsuf - # append / to directories? we are only emulating -f in a half-assed way - # here, but it''s better than nothing. - integer dirsuf=0 - # don''t be fooled by -default- >.> - if [[ -z $hsuf && "${${@//-default-/}% -# *}" == *-[[:alnum:]]#f* ]]; then - dirsuf=1 - fi - # just drop - [[ -n $__hits ]] || return - # this is the point where we have all matches in $__hits and all - # descriptions in $__dscr! - # display all matches - local dsuf dscr - for i in {1..$#__hits}; do - # add a dir suffix? - (( dirsuf )) && [[ -d $__hits[$i] ]] && dsuf=/ || dsuf= - # description to be displayed afterwards - (( $#__dscr >= $i )) && dscr=" -- ${${__dscr[$i]}##$__hits[$i] #}" || dscr= - echo -E - $IPREFIX$apre$hpre$__hits[$i]$dsuf$hsuf$asuf - done - } - # signal success! - echo ok') + # Print the first match + echo -nE - $''\0''$IPREFIX$apre$hpre$__hits[1]$dsuf$hsuf$asuf$''\0'' + } + + # Signal setup completion by sending null byte + echo $''\0'' + ') + + + # Send the string and a tab to trigger completion zpty -w $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME "$*"$'\t' - integer tog=0 - # read from the pty, and parse linewise - while zpty -r $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME; do :; done | while IFS= read -r line; do - if [[ $line == *$'\0\r' ]]; then - (( tog++ )) && return 0 || continue - fi - # display between toggles - (( tog )) && echo -E - $line - done + # Read up to the start of the first result + zpty -r $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME line '*'$'\0' - return 2 + # Read the first result + zpty -r $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME line '*'$'\0' + + # Print it, removing the trailing null byte + echo -E - ${line%$'\0'} } _zsh_autosuggest_strategy_completion() { From 0a548c62f4f57bb68cb6c45ff75b4781bb39b451 Mon Sep 17 00:00:00 2001 From: Eric Freese Date: Fri, 18 May 2018 15:24:48 -0600 Subject: [PATCH 05/15] Forgot to make after small tweak --- zsh-autosuggestions.zsh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/zsh-autosuggestions.zsh b/zsh-autosuggestions.zsh index a2134da..c29eb9e 100644 --- a/zsh-autosuggestions.zsh +++ b/zsh-autosuggestions.zsh @@ -510,7 +510,7 @@ _zsh_autosuggest_capture_completion() { setopt rcquotes () { - # Setup, blocking until null byte to signal completion + # Initialize the pty env, blocking until null byte is seen zpty -w $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME "source $1" zpty -r $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME line '*'$'\0' } =( <<< ' @@ -563,7 +563,6 @@ _zsh_autosuggest_capture_completion() { echo $''\0'' ') - # Send the string and a tab to trigger completion zpty -w $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME "$*"$'\t' From 6ffaec725a29ff2a199ceb173f72eadc42254582 Mon Sep 17 00:00:00 2001 From: Eric Freese Date: Tue, 22 May 2018 16:13:56 -0600 Subject: [PATCH 06/15] Allow completion suggestions from current shell The `zsh -f` running in the PTY doesn't know about the non-exported variables and functions defined in the original shell, thus can't make suggestions for them. Run local functions in the PTY instead of a new `zsh` process. We have to handle things differently based on whether zle is active or not (async vs. sync mode). --- src/strategies/completion.zsh | 110 +++++++++++++++++++--------------- zsh-autosuggestions.zsh | 110 +++++++++++++++++++--------------- 2 files changed, 124 insertions(+), 96 deletions(-) diff --git a/src/strategies/completion.zsh b/src/strategies/completion.zsh index aa87673..e8aac6c 100644 --- a/src/strategies/completion.zsh +++ b/src/strategies/completion.zsh @@ -3,90 +3,104 @@ # Completion Suggestion Strategy # #--------------------------------------------------------------------# # Fetches suggestions from zsh's completion engine +# Based on https://github.com/Valodim/zsh-capture-completion # -# Big thanks to https://github.com/Valodim/zsh-capture-completion -_zsh_autosuggest_capture_completion() { - zpty $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME zsh -f -i - - local line - - setopt rcquotes - () { - # Initialize the pty env, blocking until null byte is seen - zpty -w $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME "source $1" - zpty -r $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME line '*'$'\0' - } =( <<< ' - exec 2>/dev/null # Silence any error messages - - autoload compinit - compinit -d ~/.zcompdump_autosuggestions - - # Exit as soon as completion is finished - comppostfuncs=( exit ) +_zsh_autosuggest_capture_setup() { + zmodload zsh/zutil # For `zparseopts` # Never group stuff! - zstyle '':completion:*'' list-grouped false + zstyle ':completion:*' list-grouped false - # no list separator, this saves some stripping later on - zstyle '':completion:*'' list-separator '''' + # No list separator, this saves some stripping later on + zstyle ':completion:*' list-separator '' - # we use zparseopts - zmodload zsh/zutil - - # override compadd (this our hook) + # Override compadd (this is our hook) compadd () { + setopt localoptions norcexpandparam + # Just delegate and leave if any of -O, -A or -D are given if [[ ${@[1,(i)(-|--)]} == *-(O|A|D)\ * ]]; then builtin compadd "$@" return $? fi - setopt localoptions norcexpandparam extendedglob - - typeset -a __hits - # Capture completions by injecting -A parameter into the compadd call. # This takes care of matching for us. + typeset -a __hits builtin compadd -A __hits "$@" # Exit if no completion results [[ -n $__hits ]] || return - # Extract prefixes and suffixes from compadd call. we can''t do zsh''s cool - # -r remove-func magic, but it''s better than nothing. + # Extract prefixes and suffixes from compadd call. we can't do zsh's cool + # -r remove-func magic, but it's better than nothing. typeset -A apre hpre hsuf asuf zparseopts -E P:=apre p:=hpre S:=asuf s:=hsuf # Print the first match - echo -nE - $''\0''$IPREFIX$apre$hpre$__hits[1]$dsuf$hsuf$asuf$''\0'' + echo -nE - $'\0'$IPREFIX$apre$hpre$__hits[1]$dsuf$hsuf$asuf$'\0' + } +} + +_zsh_autosuggest_capture_widget() { + _zsh_autosuggest_capture_setup + + zle complete-word +} + +zle -N autosuggest-capture-completion _zsh_autosuggest_capture_widget + +_zsh_autosuggest_capture_buffer() { + local BUFFERCONTENT="$1" + + _zsh_autosuggest_capture_setup + + zmodload zsh/parameter # For `$functions` + + # Make vared completion work as if for a normal command line + # https://stackoverflow.com/a/7057118/154703 + autoload +X _complete + functions[_original_complete]=$functions[_complete] + _complete () { + unset 'compstate[vared]' + _original_complete "$@" } - # Signal setup completion by sending null byte - echo $''\0'' - ') + # Open zle with buffer set so we can capture completions for it + vared BUFFERCONTENT +} - # Send the string and a tab to trigger completion - zpty -w $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME "$*"$'\t' +_zsh_autosuggest_capture_completion() { + typeset -g completion + local line - # Read up to the start of the first result + # Zle will be inactive if we are in async mode + if zle; then + zpty $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME zle autosuggest-capture-completion + else + zpty $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME "_zsh_autosuggest_capture_buffer '$1'" + zpty -w $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME $'\t' + fi + + # The completion result is surrounded by null bytes, so read the + # content between the first two null bytes. zpty -r $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME line '*'$'\0' - - # Read the first result zpty -r $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME line '*'$'\0' + completion="${line%$'\0'}" - # Print it, removing the trailing null byte - echo -E - ${line%$'\0'} + # Destroy the pty + zpty -d $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME } _zsh_autosuggest_strategy_completion() { - typeset -g suggestion=$(_zsh_autosuggest_capture_completion "$1" | head -n 1) + typeset -g suggestion completion - # Strip the trailing carriage return - suggestion="${suggestion%$'\r'}" + # Fetch the first completion result + _zsh_autosuggest_capture_completion "$1" # Add the completion string to the buffer to build the full suggestion local -i i=1 - while [[ "$suggestion" != "${1[$i,-1]}"* ]]; do ((i++)); done - suggestion="${1[1,$i-1]}$suggestion" + while [[ "$completion" != "${1[$i,-1]}"* ]]; do ((i++)); done + suggestion="${1[1,$i-1]}$completion" } diff --git a/zsh-autosuggestions.zsh b/zsh-autosuggestions.zsh index c29eb9e..40f6f66 100644 --- a/zsh-autosuggestions.zsh +++ b/zsh-autosuggestions.zsh @@ -500,92 +500,106 @@ zle -N autosuggest-toggle _zsh_autosuggest_widget_toggle # Completion Suggestion Strategy # #--------------------------------------------------------------------# # Fetches suggestions from zsh's completion engine +# Based on https://github.com/Valodim/zsh-capture-completion # -# Big thanks to https://github.com/Valodim/zsh-capture-completion -_zsh_autosuggest_capture_completion() { - zpty $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME zsh -f -i - - local line - - setopt rcquotes - () { - # Initialize the pty env, blocking until null byte is seen - zpty -w $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME "source $1" - zpty -r $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME line '*'$'\0' - } =( <<< ' - exec 2>/dev/null # Silence any error messages - - autoload compinit - compinit -d ~/.zcompdump_autosuggestions - - # Exit as soon as completion is finished - comppostfuncs=( exit ) +_zsh_autosuggest_capture_setup() { + zmodload zsh/zutil # For `zparseopts` # Never group stuff! - zstyle '':completion:*'' list-grouped false + zstyle ':completion:*' list-grouped false - # no list separator, this saves some stripping later on - zstyle '':completion:*'' list-separator '''' + # No list separator, this saves some stripping later on + zstyle ':completion:*' list-separator '' - # we use zparseopts - zmodload zsh/zutil - - # override compadd (this our hook) + # Override compadd (this is our hook) compadd () { + setopt localoptions norcexpandparam + # Just delegate and leave if any of -O, -A or -D are given if [[ ${@[1,(i)(-|--)]} == *-(O|A|D)\ * ]]; then builtin compadd "$@" return $? fi - setopt localoptions norcexpandparam extendedglob - - typeset -a __hits - # Capture completions by injecting -A parameter into the compadd call. # This takes care of matching for us. + typeset -a __hits builtin compadd -A __hits "$@" # Exit if no completion results [[ -n $__hits ]] || return - # Extract prefixes and suffixes from compadd call. we can''t do zsh''s cool - # -r remove-func magic, but it''s better than nothing. + # Extract prefixes and suffixes from compadd call. we can't do zsh's cool + # -r remove-func magic, but it's better than nothing. typeset -A apre hpre hsuf asuf zparseopts -E P:=apre p:=hpre S:=asuf s:=hsuf # Print the first match - echo -nE - $''\0''$IPREFIX$apre$hpre$__hits[1]$dsuf$hsuf$asuf$''\0'' + echo -nE - $'\0'$IPREFIX$apre$hpre$__hits[1]$dsuf$hsuf$asuf$'\0' + } +} + +_zsh_autosuggest_capture_widget() { + _zsh_autosuggest_capture_setup + + zle complete-word +} + +zle -N autosuggest-capture-completion _zsh_autosuggest_capture_widget + +_zsh_autosuggest_capture_buffer() { + local BUFFERCONTENT="$1" + + _zsh_autosuggest_capture_setup + + zmodload zsh/parameter # For `$functions` + + # Make vared completion work as if for a normal command line + # https://stackoverflow.com/a/7057118/154703 + autoload +X _complete + functions[_original_complete]=$functions[_complete] + _complete () { + unset 'compstate[vared]' + _original_complete "$@" } - # Signal setup completion by sending null byte - echo $''\0'' - ') + # Open zle with buffer set so we can capture completions for it + vared BUFFERCONTENT +} - # Send the string and a tab to trigger completion - zpty -w $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME "$*"$'\t' +_zsh_autosuggest_capture_completion() { + typeset -g completion + local line - # Read up to the start of the first result + # Zle will be inactive if we are in async mode + if zle; then + zpty $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME zle autosuggest-capture-completion + else + zpty $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME "_zsh_autosuggest_capture_buffer '$1'" + zpty -w $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME $'\t' + fi + + # The completion result is surrounded by null bytes, so read the + # content between the first two null bytes. zpty -r $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME line '*'$'\0' - - # Read the first result zpty -r $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME line '*'$'\0' + completion="${line%$'\0'}" - # Print it, removing the trailing null byte - echo -E - ${line%$'\0'} + # Destroy the pty + zpty -d $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME } _zsh_autosuggest_strategy_completion() { - typeset -g suggestion=$(_zsh_autosuggest_capture_completion "$1" | head -n 1) + typeset -g suggestion completion - # Strip the trailing carriage return - suggestion="${suggestion%$'\r'}" + # Fetch the first completion result + _zsh_autosuggest_capture_completion "$1" # Add the completion string to the buffer to build the full suggestion local -i i=1 - while [[ "$suggestion" != "${1[$i,-1]}"* ]]; do ((i++)); done - suggestion="${1[1,$i-1]}$suggestion" + while [[ "$completion" != "${1[$i,-1]}"* ]]; do ((i++)); done + suggestion="${1[1,$i-1]}$completion" } #--------------------------------------------------------------------# From 3dbd9afaec0a5f3c1c63779d665b1dcf93b60c7f Mon Sep 17 00:00:00 2001 From: Eric Freese Date: Tue, 22 May 2018 17:16:45 -0600 Subject: [PATCH 07/15] Fix completion strategy killing other pty's Only a problem in synchronous mode --- spec/integrations/client_zpty_spec.rb | 21 +++++++++++++++++---- src/strategies/completion.zsh | 10 ++++++++++ zsh-autosuggestions.zsh | 10 ++++++++++ 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/spec/integrations/client_zpty_spec.rb b/spec/integrations/client_zpty_spec.rb index 8f1550e..e364759 100644 --- a/spec/integrations/client_zpty_spec.rb +++ b/spec/integrations/client_zpty_spec.rb @@ -1,10 +1,23 @@ describe 'a running zpty command' do let(:before_sourcing) { -> { session.run_command('zmodload zsh/zpty && zpty -b kitty cat') } } - it 'is not affected by running zsh-autosuggestions' do - sleep 1 # Give a little time for precmd hooks to run - session.run_command('zpty -t kitty; echo $?') + context 'when sourcing the plugin' do + it 'is not affected' do + sleep 1 # Give a little time for precmd hooks to run + session.run_command('zpty -t kitty; echo $?') - wait_for { session.content }.to end_with("\n0") + wait_for { session.content }.to end_with("\n0") + end + end + + context 'when using `completion` strategy' do + let(:options) { ["ZSH_AUTOSUGGEST_STRATEGY=completion"] } + + it 'is not affected' do + session.send_keys('a').send_keys('C-h') + session.run_command('zpty -t kitty; echo $?') + + wait_for { session.content }.to end_with("\n0") + end end end diff --git a/src/strategies/completion.zsh b/src/strategies/completion.zsh index e8aac6c..6517444 100644 --- a/src/strategies/completion.zsh +++ b/src/strategies/completion.zsh @@ -9,6 +9,16 @@ _zsh_autosuggest_capture_setup() { zmodload zsh/zutil # For `zparseopts` + # There is a bug in zpty module (fixed in zsh/master) by which a + # zpty that exits will kill all zpty processes that were forked + # before it. Here we set up a zsh exit hook to SIGKILL the zpty + # process immediately, before it has a chance to kill any other + # zpty processes. + zshexit() { + kill -KILL $$ + sleep 1 # Block for long enough for the signal to come through + } + # Never group stuff! zstyle ':completion:*' list-grouped false diff --git a/zsh-autosuggestions.zsh b/zsh-autosuggestions.zsh index 40f6f66..644bacf 100644 --- a/zsh-autosuggestions.zsh +++ b/zsh-autosuggestions.zsh @@ -506,6 +506,16 @@ zle -N autosuggest-toggle _zsh_autosuggest_widget_toggle _zsh_autosuggest_capture_setup() { zmodload zsh/zutil # For `zparseopts` + # There is a bug in zpty module (fixed in zsh/master) by which a + # zpty that exits will kill all zpty processes that were forked + # before it. Here we set up a zsh exit hook to SIGKILL the zpty + # process immediately, before it has a chance to kill any other + # zpty processes. + zshexit() { + kill -KILL $$ + sleep 1 # Block for long enough for the signal to come through + } + # Never group stuff! zstyle ':completion:*' list-grouped false From cf458d2a3bcc494a1ab63fdb542f528b42b71546 Mon Sep 17 00:00:00 2001 From: Eric Freese Date: Wed, 23 May 2018 14:50:56 -0600 Subject: [PATCH 08/15] Fix completion suggestions when compinit is not enabled Need to make sure compinit is called in the pty or the shell hangs --- src/strategies/completion.zsh | 2 ++ zsh-autosuggestions.zsh | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/strategies/completion.zsh b/src/strategies/completion.zsh index 6517444..c8b176b 100644 --- a/src/strategies/completion.zsh +++ b/src/strategies/completion.zsh @@ -9,6 +9,8 @@ _zsh_autosuggest_capture_setup() { zmodload zsh/zutil # For `zparseopts` + autoload compinit && compinit + # There is a bug in zpty module (fixed in zsh/master) by which a # zpty that exits will kill all zpty processes that were forked # before it. Here we set up a zsh exit hook to SIGKILL the zpty diff --git a/zsh-autosuggestions.zsh b/zsh-autosuggestions.zsh index 644bacf..756cfbc 100644 --- a/zsh-autosuggestions.zsh +++ b/zsh-autosuggestions.zsh @@ -506,6 +506,8 @@ zle -N autosuggest-toggle _zsh_autosuggest_widget_toggle _zsh_autosuggest_capture_setup() { zmodload zsh/zutil # For `zparseopts` + autoload compinit && compinit + # There is a bug in zpty module (fixed in zsh/master) by which a # zpty that exits will kill all zpty processes that were forked # before it. Here we set up a zsh exit hook to SIGKILL the zpty From 7d19f8f9b2bf03b92372f95d37f5b55a6e941684 Mon Sep 17 00:00:00 2001 From: Eric Freese Date: Wed, 23 May 2018 22:04:16 -0600 Subject: [PATCH 09/15] Rename default spec to history spec --- spec/strategies/{default_spec.rb => history_spec.rb} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename spec/strategies/{default_spec.rb => history_spec.rb} (85%) diff --git a/spec/strategies/default_spec.rb b/spec/strategies/history_spec.rb similarity index 85% rename from spec/strategies/default_spec.rb rename to spec/strategies/history_spec.rb index 89321f3..f8ae526 100644 --- a/spec/strategies/default_spec.rb +++ b/spec/strategies/history_spec.rb @@ -1,6 +1,6 @@ require 'strategies/special_characters_helper' -describe 'the default suggestion strategy' do +describe 'the `history` suggestion strategy' do it 'suggests the last matching history entry' do with_history('ls foo', 'ls bar', 'echo baz') do session.send_string('ls') From 973205005cca59f0701e6379474bf24eef805ac1 Mon Sep 17 00:00:00 2001 From: Eric Freese Date: Wed, 23 May 2018 22:04:47 -0600 Subject: [PATCH 10/15] Add spec for `completion` strategy --- spec/strategies/completion_spec.rb | 30 ++++++++++++++++++++++++++ spec/strategies/match_prev_cmd_spec.rb | 2 +- src/strategies/completion.zsh | 5 ++++- zsh-autosuggestions.zsh | 5 ++++- 4 files changed, 39 insertions(+), 3 deletions(-) create mode 100644 spec/strategies/completion_spec.rb diff --git a/spec/strategies/completion_spec.rb b/spec/strategies/completion_spec.rb new file mode 100644 index 0000000..62cf0e5 --- /dev/null +++ b/spec/strategies/completion_spec.rb @@ -0,0 +1,30 @@ +describe 'the `completion` suggestion strategy' do + let(:options) { ['ZSH_AUTOSUGGEST_STRATEGY=completion'] } + let(:before_sourcing) do + -> do + session. + run_command('autoload compinit && compinit'). + run_command('_foo() { compadd bar }'). + run_command('compdef _foo baz') + end + end + + it 'suggests the first completion result' do + session.send_string('baz ') + wait_for { session.content }.to eq('baz bar') + end + + context 'when async mode is enabled' do + before do + skip 'Async mode not supported below v5.0.8' if session.zsh_version < Gem::Version.new('5.0.8') + end + + let(:options) { ['ZSH_AUTOSUGGEST_USE_ASYNC=true', 'ZSH_AUTOSUGGEST_STRATEGY=completion'] } + + it 'suggests the first completion result' do + session.send_string('baz ') + wait_for { session.content }.to eq('baz bar') + end + end +end + diff --git a/spec/strategies/match_prev_cmd_spec.rb b/spec/strategies/match_prev_cmd_spec.rb index f1596ba..5a143b8 100644 --- a/spec/strategies/match_prev_cmd_spec.rb +++ b/spec/strategies/match_prev_cmd_spec.rb @@ -1,6 +1,6 @@ require 'strategies/special_characters_helper' -describe 'the match_prev_cmd strategy' do +describe 'the `match_prev_cmd` strategy' do let(:options) { ['ZSH_AUTOSUGGEST_STRATEGY=match_prev_cmd'] } it 'suggests the last matching history entry after the previous command' do diff --git a/src/strategies/completion.zsh b/src/strategies/completion.zsh index c8b176b..0808575 100644 --- a/src/strategies/completion.zsh +++ b/src/strategies/completion.zsh @@ -9,7 +9,10 @@ _zsh_autosuggest_capture_setup() { zmodload zsh/zutil # For `zparseopts` - autoload compinit && compinit + # Ensure completions have been initialized + if ! whence compdef >/dev/null; then + autoload -Uz compinit && compinit + fi # There is a bug in zpty module (fixed in zsh/master) by which a # zpty that exits will kill all zpty processes that were forked diff --git a/zsh-autosuggestions.zsh b/zsh-autosuggestions.zsh index 756cfbc..da10235 100644 --- a/zsh-autosuggestions.zsh +++ b/zsh-autosuggestions.zsh @@ -506,7 +506,10 @@ zle -N autosuggest-toggle _zsh_autosuggest_widget_toggle _zsh_autosuggest_capture_setup() { zmodload zsh/zutil # For `zparseopts` - autoload compinit && compinit + # Ensure completions have been initialized + if ! whence compdef >/dev/null; then + autoload -Uz compinit && compinit + fi # There is a bug in zpty module (fixed in zsh/master) by which a # zpty that exits will kill all zpty processes that were forked From 949c37419544e6fab313d5139723fa315d645cab Mon Sep 17 00:00:00 2001 From: Eric Freese Date: Thu, 24 May 2018 16:45:20 -0600 Subject: [PATCH 11/15] Fix `completion` strategy on older versions of zsh `zpty -r` with a pattern seems to have some funky behavior on older versions, giving unpredictable results --- src/strategies/completion.zsh | 8 +++++--- zsh-autosuggestions.zsh | 8 +++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/strategies/completion.zsh b/src/strategies/completion.zsh index 0808575..a23b630 100644 --- a/src/strategies/completion.zsh +++ b/src/strategies/completion.zsh @@ -100,9 +100,11 @@ _zsh_autosuggest_capture_completion() { # The completion result is surrounded by null bytes, so read the # content between the first two null bytes. - zpty -r $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME line '*'$'\0' - zpty -r $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME line '*'$'\0' - completion="${line%$'\0'}" + zpty -r $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME line '*'$'\0''*'$'\0' + + # On older versions of zsh, we sometimes get extra bytes after the + # second null byte, so trim those off the end + completion="${${${(M)line:#*$'\0'*$'\0'*}#*$'\0'}%%$'\0'*}" # Destroy the pty zpty -d $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME diff --git a/zsh-autosuggestions.zsh b/zsh-autosuggestions.zsh index da10235..6683262 100644 --- a/zsh-autosuggestions.zsh +++ b/zsh-autosuggestions.zsh @@ -597,9 +597,11 @@ _zsh_autosuggest_capture_completion() { # The completion result is surrounded by null bytes, so read the # content between the first two null bytes. - zpty -r $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME line '*'$'\0' - zpty -r $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME line '*'$'\0' - completion="${line%$'\0'}" + zpty -r $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME line '*'$'\0''*'$'\0' + + # On older versions of zsh, we sometimes get extra bytes after the + # second null byte, so trim those off the end + completion="${${${(M)line:#*$'\0'*$'\0'*}#*$'\0'}%%$'\0'*}" # Destroy the pty zpty -d $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME From bcbdad83e940917db9bbd0afd62057229446c00f Mon Sep 17 00:00:00 2001 From: Eric Freese Date: Wed, 6 Jun 2018 21:49:03 -0600 Subject: [PATCH 12/15] Support fallback strategies by setting array in config --- Makefile | 1 + README.md | 11 ++++-- spec/integrations/zle_input_stack_spec.rb | 2 +- spec/options/strategy_spec.rb | 45 ++++++++++++++++----- src/async.zsh | 2 +- src/config.zsh | 4 +- src/fetch.zsh | 23 +++++++++++ src/strategies/default.zsh | 17 -------- src/widgets.zsh | 2 +- zsh-autosuggestions.zsh | 48 +++++++++++++---------- 10 files changed, 100 insertions(+), 55 deletions(-) create mode 100644 src/fetch.zsh delete mode 100644 src/strategies/default.zsh diff --git a/Makefile b/Makefile index d5d162c..b89ff04 100644 --- a/Makefile +++ b/Makefile @@ -9,6 +9,7 @@ SRC_FILES := \ $(SRC_DIR)/highlight.zsh \ $(SRC_DIR)/widgets.zsh \ $(SRC_DIR)/strategies/*.zsh \ + $(SRC_DIR)/fetch.zsh \ $(SRC_DIR)/async.zsh \ $(SRC_DIR)/start.zsh diff --git a/README.md b/README.md index 4ad07d8..5039b53 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ _[Fish](http://fishshell.com/)-like fast/unobtrusive autosuggestions for zsh._ -It suggests commands as you type, based on command history. +It suggests commands as you type. Requirements: Zsh v4.3.11 or later @@ -39,10 +39,13 @@ Set `ZSH_AUTOSUGGEST_HIGHLIGHT_STYLE` to configure the style that the suggestion ### Suggestion Strategy -Set `ZSH_AUTOSUGGEST_STRATEGY` to choose the strategy for generating suggestions. There are currently two to choose from: +`ZSH_AUTOSUGGEST_STRATEGY` is an array that specifies how suggestions should be generated. The strategies in the array are tried successively until a suggestion is found. There are currently three built-in strategies to choose from: -- `default`: Chooses the most recent match. -- `match_prev_cmd`: Chooses the most recent match whose preceding history item matches the most recently executed command ([more info](src/strategies/match_prev_cmd.zsh)). Note that this strategy won't work as expected with ZSH options that don't preserve the history order such as `HIST_IGNORE_ALL_DUPS` or `HIST_EXPIRE_DUPS_FIRST`. +- `history`: Chooses the most recent match from history. +- `match_prev_cmd`: Like `history`, but chooses the most recent match whose preceding history item matches the most recently executed command ([more info](src/strategies/match_prev_cmd.zsh)). Note that this strategy won't work as expected with ZSH options that don't preserve the history order such as `HIST_IGNORE_ALL_DUPS` or `HIST_EXPIRE_DUPS_FIRST`. +- `completion`: (experimental) Chooses a suggestion based on what tab-completion would suggest. + +For example, setting `ZSH_AUTOSUGGEST_STRATEGY=(history completion)` will first try to find a suggestion from your history, but, if it can't find a match, will find a suggestion from the completion engine. ### Widget Mapping diff --git a/spec/integrations/zle_input_stack_spec.rb b/spec/integrations/zle_input_stack_spec.rb index 8a2c990..12cfbc7 100644 --- a/spec/integrations/zle_input_stack_spec.rb +++ b/spec/integrations/zle_input_stack_spec.rb @@ -2,7 +2,7 @@ describe 'using `zle -U`' do let(:before_sourcing) do -> do session. - run_command('_zsh_autosuggest_strategy_test() { sleep 1; _zsh_autosuggest_strategy_default "$1" }'). + run_command('_zsh_autosuggest_strategy_test() { sleep 1; _zsh_autosuggest_strategy_history "$1" }'). run_command('foo() { zle -U - "echo hello" }; zle -N foo; bindkey ^B foo') end end diff --git a/spec/options/strategy_spec.rb b/spec/options/strategy_spec.rb index c9f01e1..378d01e 100644 --- a/spec/options/strategy_spec.rb +++ b/spec/options/strategy_spec.rb @@ -1,20 +1,45 @@ describe 'a suggestion for a given prefix' do - let(:options) { ['_zsh_autosuggest_strategy_default() { suggestion="echo foo" }'] } + let(:history_strategy) { '_zsh_autosuggest_strategy_history() { suggestion="history" }' } + let(:foobar_strategy) { '_zsh_autosuggest_strategy_foobar() { [[ "foobar baz" = $1* ]] && suggestion="foobar baz" }' } + let(:foobaz_strategy) { '_zsh_autosuggest_strategy_foobaz() { [[ "foobaz bar" = $1* ]] && suggestion="foobaz bar" }' } - it 'is determined by calling the default strategy function' do - session.send_string('e') - wait_for { session.content }.to eq('echo foo') + let(:options) { [ history_strategy ] } + + it 'by default is determined by calling the `history` strategy function' do + session.send_string('h') + wait_for { session.content }.to eq('history') end - context 'when ZSH_AUTOSUGGEST_STRATEGY is set' do + context 'when ZSH_AUTOSUGGEST_STRATEGY is set to an array' do let(:options) { [ - '_zsh_autosuggest_strategy_custom() { suggestion="echo foo" }', - 'ZSH_AUTOSUGGEST_STRATEGY=custom' + foobar_strategy, + foobaz_strategy, + 'ZSH_AUTOSUGGEST_STRATEGY=(foobar foobaz)' ] } - it 'is determined by calling the specified strategy function' do - session.send_string('e') - wait_for { session.content }.to eq('echo foo') + it 'is determined by the first strategy function to return a suggestion' do + session.send_string('foo') + wait_for { session.content }.to eq('foobar baz') + + session.send_string('baz') + wait_for { session.content }.to eq('foobaz bar') + end + end + + context 'when ZSH_AUTOSUGGEST_STRATEGY is set to a string' do + let(:options) { [ + foobar_strategy, + foobaz_strategy, + 'ZSH_AUTOSUGGEST_STRATEGY="foobar foobaz"' + ] } + + it 'is determined by the first strategy function to return a suggestion' do + session.send_string('foo') + wait_for { session.content }.to eq('foobar baz') + + session.send_string('baz') + wait_for { session.content }.to eq('foobaz bar') end end end + diff --git a/src/async.zsh b/src/async.zsh index 62e3329..dd54c24 100644 --- a/src/async.zsh +++ b/src/async.zsh @@ -35,7 +35,7 @@ _zsh_autosuggest_async_server() { # Run suggestion search in the background ( local suggestion - _zsh_autosuggest_strategy_$ZSH_AUTOSUGGEST_STRATEGY "$query" + _zsh_autosuggest_fetch_suggestion "$query" echo -n -E "$suggestion"$'\0' ) & diff --git a/src/config.zsh b/src/config.zsh index 9b3dbae..4598191 100644 --- a/src/config.zsh +++ b/src/config.zsh @@ -11,7 +11,9 @@ ZSH_AUTOSUGGEST_HIGHLIGHT_STYLE='fg=8' # Prefix to use when saving original versions of bound widgets ZSH_AUTOSUGGEST_ORIGINAL_WIDGET_PREFIX=autosuggest-orig- -ZSH_AUTOSUGGEST_STRATEGY=default +# Strategies to use to fetch a suggestion +# Will try each strategy in order until a suggestion is returned +ZSH_AUTOSUGGEST_STRATEGY=(history) # Widgets that clear the suggestion ZSH_AUTOSUGGEST_CLEAR_WIDGETS=( diff --git a/src/fetch.zsh b/src/fetch.zsh new file mode 100644 index 0000000..f94e66d --- /dev/null +++ b/src/fetch.zsh @@ -0,0 +1,23 @@ + +#--------------------------------------------------------------------# +# Fetch Suggestion # +#--------------------------------------------------------------------# +# Loops through all specified strategies and returns a suggestion +# from the first strategy to provide one. +# + +_zsh_autosuggest_fetch_suggestion() { + typeset -g suggestion + local -a strategies + + # Ensure we are working with an array + strategies=(${=ZSH_AUTOSUGGEST_STRATEGY}) + + for strategy in $strategies; do + # Try to get a suggestion from this strategy + _zsh_autosuggest_strategy_$strategy "$1" + + # Break once we've found a suggestion + [[ -n "$suggestion" ]] && break + done +} diff --git a/src/strategies/default.zsh b/src/strategies/default.zsh deleted file mode 100644 index 68617ff..0000000 --- a/src/strategies/default.zsh +++ /dev/null @@ -1,17 +0,0 @@ - -#--------------------------------------------------------------------# -# Default Suggestion Strategy # -#--------------------------------------------------------------------# -# Will provide suggestions from your history. If no matches are found -# in history, will provide a suggestion from the completion engine. -# - -_zsh_autosuggest_strategy_default() { - typeset -g suggestion - - _zsh_autosuggest_strategy_history "$1" - - if [[ -z "$suggestion" ]]; then - _zsh_autosuggest_strategy_completion "$1" - fi -} diff --git a/src/widgets.zsh b/src/widgets.zsh index 87bb62e..89af395 100644 --- a/src/widgets.zsh +++ b/src/widgets.zsh @@ -97,7 +97,7 @@ _zsh_autosuggest_fetch() { _zsh_autosuggest_async_request "$BUFFER" else local suggestion - _zsh_autosuggest_strategy_$ZSH_AUTOSUGGEST_STRATEGY "$BUFFER" + _zsh_autosuggest_fetch_suggestion "$BUFFER" _zsh_autosuggest_suggest "$suggestion" fi } diff --git a/zsh-autosuggestions.zsh b/zsh-autosuggestions.zsh index 6683262..02d4b2f 100644 --- a/zsh-autosuggestions.zsh +++ b/zsh-autosuggestions.zsh @@ -47,7 +47,9 @@ ZSH_AUTOSUGGEST_HIGHLIGHT_STYLE='fg=8' # Prefix to use when saving original versions of bound widgets ZSH_AUTOSUGGEST_ORIGINAL_WIDGET_PREFIX=autosuggest-orig- -ZSH_AUTOSUGGEST_STRATEGY=default +# Strategies to use to fetch a suggestion +# Will try each strategy in order until a suggestion is returned +ZSH_AUTOSUGGEST_STRATEGY=(history) # Widgets that clear the suggestion ZSH_AUTOSUGGEST_CLEAR_WIDGETS=( @@ -381,7 +383,7 @@ _zsh_autosuggest_fetch() { _zsh_autosuggest_async_request "$BUFFER" else local suggestion - _zsh_autosuggest_strategy_$ZSH_AUTOSUGGEST_STRATEGY "$BUFFER" + _zsh_autosuggest_fetch_suggestion "$BUFFER" _zsh_autosuggest_suggest "$suggestion" fi } @@ -619,23 +621,6 @@ _zsh_autosuggest_strategy_completion() { suggestion="${1[1,$i-1]}$completion" } -#--------------------------------------------------------------------# -# Default Suggestion Strategy # -#--------------------------------------------------------------------# -# Will provide suggestions from your history. If no matches are found -# in history, will provide a suggestion from the completion engine. -# - -_zsh_autosuggest_strategy_default() { - typeset -g suggestion - - _zsh_autosuggest_strategy_history "$1" - - if [[ -z "$suggestion" ]]; then - _zsh_autosuggest_strategy_completion "$1" - fi -} - #--------------------------------------------------------------------# # History Suggestion Strategy # #--------------------------------------------------------------------# @@ -720,6 +705,29 @@ _zsh_autosuggest_strategy_match_prev_cmd() { typeset -g suggestion="$history[$histkey]" } +#--------------------------------------------------------------------# +# Fetch Suggestion # +#--------------------------------------------------------------------# +# Loops through all specified strategies and returns a suggestion +# from the first strategy to provide one. +# + +_zsh_autosuggest_fetch_suggestion() { + typeset -g suggestion + local -a strategies + + # Ensure we are working with an array + strategies=(${=ZSH_AUTOSUGGEST_STRATEGY}) + + for strategy in $strategies; do + # Try to get a suggestion from this strategy + _zsh_autosuggest_strategy_$strategy "$1" + + # Break once we've found a suggestion + [[ -n "$suggestion" ]] && break + done +} + #--------------------------------------------------------------------# # Async # #--------------------------------------------------------------------# @@ -756,7 +764,7 @@ _zsh_autosuggest_async_server() { # Run suggestion search in the background ( local suggestion - _zsh_autosuggest_strategy_$ZSH_AUTOSUGGEST_STRATEGY "$query" + _zsh_autosuggest_fetch_suggestion "$query" echo -n -E "$suggestion"$'\0' ) & From b0ffc34fb83136b29e6ece5028f0fda6b1e00ee7 Mon Sep 17 00:00:00 2001 From: Eric Freese Date: Sun, 10 Jun 2018 23:35:22 -0600 Subject: [PATCH 13/15] completion should be a local var --- src/strategies/completion.zsh | 3 ++- zsh-autosuggestions.zsh | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/strategies/completion.zsh b/src/strategies/completion.zsh index a23b630..847f22b 100644 --- a/src/strategies/completion.zsh +++ b/src/strategies/completion.zsh @@ -111,7 +111,8 @@ _zsh_autosuggest_capture_completion() { } _zsh_autosuggest_strategy_completion() { - typeset -g suggestion completion + typeset -g suggestion + local completion # Fetch the first completion result _zsh_autosuggest_capture_completion "$1" diff --git a/zsh-autosuggestions.zsh b/zsh-autosuggestions.zsh index 02d4b2f..823f541 100644 --- a/zsh-autosuggestions.zsh +++ b/zsh-autosuggestions.zsh @@ -610,7 +610,8 @@ _zsh_autosuggest_capture_completion() { } _zsh_autosuggest_strategy_completion() { - typeset -g suggestion completion + typeset -g suggestion + local completion # Fetch the first completion result _zsh_autosuggest_capture_completion "$1" From 43a011026ffd9c7e9dba4cf022922a4877f1d4e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20V=C3=A4th?= Date: Mon, 18 Jun 2018 19:47:27 +0200 Subject: [PATCH 14/15] Do not leak global variables REPLY and strategy https://github.com/zsh-users/zsh-autosuggestions/issues/341 --- src/fetch.zsh | 1 + src/strategies/completion.zsh | 2 +- zsh-autosuggestions.zsh | 3 ++- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/fetch.zsh b/src/fetch.zsh index f94e66d..1517018 100644 --- a/src/fetch.zsh +++ b/src/fetch.zsh @@ -9,6 +9,7 @@ _zsh_autosuggest_fetch_suggestion() { typeset -g suggestion local -a strategies + local strategy # Ensure we are working with an array strategies=(${=ZSH_AUTOSUGGEST_STRATEGY}) diff --git a/src/strategies/completion.zsh b/src/strategies/completion.zsh index 847f22b..30542ee 100644 --- a/src/strategies/completion.zsh +++ b/src/strategies/completion.zsh @@ -88,7 +88,7 @@ _zsh_autosuggest_capture_buffer() { _zsh_autosuggest_capture_completion() { typeset -g completion - local line + local line REPLY # Zle will be inactive if we are in async mode if zle; then diff --git a/zsh-autosuggestions.zsh b/zsh-autosuggestions.zsh index 8aad4c8..17e0d52 100644 --- a/zsh-autosuggestions.zsh +++ b/zsh-autosuggestions.zsh @@ -587,7 +587,7 @@ _zsh_autosuggest_capture_buffer() { _zsh_autosuggest_capture_completion() { typeset -g completion - local line + local line REPLY # Zle will be inactive if we are in async mode if zle; then @@ -716,6 +716,7 @@ _zsh_autosuggest_strategy_match_prev_cmd() { _zsh_autosuggest_fetch_suggestion() { typeset -g suggestion local -a strategies + local strategy # Ensure we are working with an array strategies=(${=ZSH_AUTOSUGGEST_STRATEGY}) From 1ec43c7291db3327391360e466907329b36c6770 Mon Sep 17 00:00:00 2001 From: Eric Freese Date: Fri, 29 Jun 2018 22:08:33 -0600 Subject: [PATCH 15/15] Fix error when single quote entered into buffer Error looked something like: ``` % echo 'f(zpty):8: unmatched ' _zsh_autosuggest_capture_completion:zpty:9: no such pty command: zsh_autosuggest_completion_pty _zsh_autosuggest_capture_completion:zpty:14: no such pty command: zsh_autosuggest_completion_pty _zsh_autosuggest_capture_completion:zpty:21: no such pty command: zsh_autosuggest_completion_pty ``` According to `man zshmodules`, the args to `zpty` are "concatenated with spaces between, then executed as a command, as if passed to the eval builtin." So we need to escape the `$` so that `$1` is passed to eval instead of the value of `$1`. --- src/strategies/completion.zsh | 2 +- zsh-autosuggestions.zsh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/strategies/completion.zsh b/src/strategies/completion.zsh index 30542ee..8bbd7c8 100644 --- a/src/strategies/completion.zsh +++ b/src/strategies/completion.zsh @@ -94,7 +94,7 @@ _zsh_autosuggest_capture_completion() { if zle; then zpty $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME zle autosuggest-capture-completion else - zpty $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME "_zsh_autosuggest_capture_buffer '$1'" + zpty $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME _zsh_autosuggest_capture_buffer "\$1" zpty -w $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME $'\t' fi diff --git a/zsh-autosuggestions.zsh b/zsh-autosuggestions.zsh index 17e0d52..6f27260 100644 --- a/zsh-autosuggestions.zsh +++ b/zsh-autosuggestions.zsh @@ -593,7 +593,7 @@ _zsh_autosuggest_capture_completion() { if zle; then zpty $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME zle autosuggest-capture-completion else - zpty $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME "_zsh_autosuggest_capture_buffer '$1'" + zpty $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME _zsh_autosuggest_capture_buffer "\$1" zpty -w $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME $'\t' fi