diff --git a/spec/strategies/completion_spec.rb b/spec/strategies/completion_spec.rb index bd2c72d..e8cc8ce 100644 --- a/spec/strategies/completion_spec.rb +++ b/spec/strategies/completion_spec.rb @@ -4,7 +4,7 @@ describe 'the `completion` suggestion strategy' do -> do session. run_command('autoload compinit && compinit'). - run_command('_foo() { compadd bar }'). + run_command('_foo() { compadd bar; compadd bat }'). run_command('compdef _foo baz') end end @@ -14,6 +14,12 @@ describe 'the `completion` suggestion strategy' do wait_for { session.content }.to eq('baz bar') end + it 'does not add extra carriage returns when prefix has a line feed' do + skip '`stty` does not work inside zpty below zsh version 5.0.3' if session.zsh_version < Gem::Version.new('5.0.3') + session.send_string('baz \\').send_keys('C-v', 'C-j') + wait_for { session.content }.to eq("baz \\\nbar") + end + context 'when async mode is enabled' do let(:options) { ['ZSH_AUTOSUGGEST_USE_ASYNC=true', 'ZSH_AUTOSUGGEST_STRATEGY=completion'] } @@ -21,6 +27,12 @@ describe 'the `completion` suggestion strategy' do session.send_string('baz ') wait_for { session.content }.to eq('baz bar') end + + it 'does not add extra carriage returns when prefix has a line feed' do + skip '`stty` does not work inside zpty below zsh version 5.0.3' if session.zsh_version < Gem::Version.new('5.0.3') + session.send_string('baz \\').send_keys('C-v', 'C-j') + wait_for { session.content }.to eq("baz \\\nbar") + end end end diff --git a/src/async.zsh b/src/async.zsh index b038cb0..d7e9fe8 100644 --- a/src/async.zsh +++ b/src/async.zsh @@ -56,9 +56,12 @@ _zsh_autosuggest_async_request() { _zsh_autosuggest_async_response() { emulate -L zsh + local suggestion + if [[ -z "$2" || "$2" == "hup" ]]; then # Read everything from the fd and give it as a suggestion - zle autosuggest-suggest -- "$(cat <&$1)" + IFS='' read -rd '' -u $1 suggestion + zle autosuggest-suggest -- "$suggestion" # Close the fd exec {1}<&- diff --git a/src/strategies/completion.zsh b/src/strategies/completion.zsh index bf4ed9a..4c60e90 100644 --- a/src/strategies/completion.zsh +++ b/src/strategies/completion.zsh @@ -10,10 +10,13 @@ _zsh_autosuggest_capture_postcompletion() { compstate[insert]=1 # Don't list completions - unset compstate[list] + unset 'compstate[list]' } _zsh_autosuggest_capture_completion_widget() { + # Add a post-completion hook to be called after all completions have been + # gathered. The hook can modify compstate to affect what is done with the + # gathered completions. local -a +h comppostfuncs comppostfuncs=(_zsh_autosuggest_capture_postcompletion) @@ -25,6 +28,16 @@ _zsh_autosuggest_capture_completion_widget() { # after autosuggestions is initialized. zle -- ${(k)widgets[(r)completion:.complete-word:_main_complete]} + if is-at-least 5.0.3; then + # Don't do any cr/lf transformations. We need to do this immediately before + # output because if we do it in setup, onlcr will be re-enabled when we enter + # vared in the async code path. There is a bug in zpty module in older versions + # where the tty is not properly attached to the pty slave, resulting in stty + # getting stopped with a SIGTTOU. See zsh-workers thread 31660 and upstream + # commit f75904a38 + stty -onlcr -ocrnl -F /dev/tty + fi + # The completion has been added, print the buffer as the suggestion echo -nE - $'\0'$BUFFER$'\0' } @@ -32,6 +45,8 @@ _zsh_autosuggest_capture_completion_widget() { zle -N autosuggest-capture-completion _zsh_autosuggest_capture_completion_widget _zsh_autosuggest_capture_setup() { + autoload -Uz is-at-least + # There is a bug in zpty module in older zsh versions 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 @@ -39,8 +54,12 @@ _zsh_autosuggest_capture_setup() { # zpty processes. if ! is-at-least 5.4; then zshexit() { - kill -KILL $$ - sleep 1 # Block for long enough for the signal to come through + # The zsh builtin `kill` fails sometimes in older versions + # https://unix.stackexchange.com/a/477647/156673 + kill -KILL $$ 2>&- || command kill -KILL $$ + + # Block for long enough for the signal to come through + sleep 1 } fi @@ -99,9 +118,11 @@ _zsh_autosuggest_strategy_completion() { # content between the first two null bytes. 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 - suggestion="${${${(M)line:#*$'\0'*$'\0'*}#*$'\0'}%%$'\0'*}" + # Extract the suggestion from between the null bytes. On older + # versions of zsh (older than 5.3), we sometimes get extra bytes after + # the second null byte, so trim those off the end. + # See http://www.zsh.org/mla/workers/2015/msg03290.html + suggestion="${${line#*$'\0'}%$'\0'*}" } always { # Destroy the pty zpty -d $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME diff --git a/zsh-autosuggestions.zsh b/zsh-autosuggestions.zsh index f2fe337..5f2401d 100644 --- a/zsh-autosuggestions.zsh +++ b/zsh-autosuggestions.zsh @@ -492,10 +492,13 @@ _zsh_autosuggest_capture_postcompletion() { compstate[insert]=1 # Don't list completions - unset compstate[list] + unset 'compstate[list]' } _zsh_autosuggest_capture_completion_widget() { + # Add a post-completion hook to be called after all completions have been + # gathered. The hook can modify compstate to affect what is done with the + # gathered completions. local -a +h comppostfuncs comppostfuncs=(_zsh_autosuggest_capture_postcompletion) @@ -507,6 +510,16 @@ _zsh_autosuggest_capture_completion_widget() { # after autosuggestions is initialized. zle -- ${(k)widgets[(r)completion:.complete-word:_main_complete]} + if is-at-least 5.0.3; then + # Don't do any cr/lf transformations. We need to do this immediately before + # output because if we do it in setup, onlcr will be re-enabled when we enter + # vared in the async code path. There is a bug in zpty module in older versions + # where the tty is not properly attached to the pty slave, resulting in stty + # getting stopped with a SIGTTOU. See zsh-workers thread 31660 and upstream + # commit f75904a38 + stty -onlcr -ocrnl -F /dev/tty + fi + # The completion has been added, print the buffer as the suggestion echo -nE - $'\0'$BUFFER$'\0' } @@ -514,6 +527,8 @@ _zsh_autosuggest_capture_completion_widget() { zle -N autosuggest-capture-completion _zsh_autosuggest_capture_completion_widget _zsh_autosuggest_capture_setup() { + autoload -Uz is-at-least + # There is a bug in zpty module in older zsh versions 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 @@ -521,8 +536,12 @@ _zsh_autosuggest_capture_setup() { # zpty processes. if ! is-at-least 5.4; then zshexit() { - kill -KILL $$ - sleep 1 # Block for long enough for the signal to come through + # The zsh builtin `kill` fails sometimes in older versions + # https://unix.stackexchange.com/a/477647/156673 + kill -KILL $$ 2>&- || command kill -KILL $$ + + # Block for long enough for the signal to come through + sleep 1 } fi @@ -581,9 +600,11 @@ _zsh_autosuggest_strategy_completion() { # content between the first two null bytes. 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 - suggestion="${${${(M)line:#*$'\0'*$'\0'*}#*$'\0'}%%$'\0'*}" + # Extract the suggestion from between the null bytes. On older + # versions of zsh (older than 5.3), we sometimes get extra bytes after + # the second null byte, so trim those off the end. + # See http://www.zsh.org/mla/workers/2015/msg03290.html + suggestion="${${line#*$'\0'}%$'\0'*}" } always { # Destroy the pty zpty -d $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME @@ -758,9 +779,12 @@ _zsh_autosuggest_async_request() { _zsh_autosuggest_async_response() { emulate -L zsh + local suggestion + if [[ -z "$2" || "$2" == "hup" ]]; then # Read everything from the fd and give it as a suggestion - zle autosuggest-suggest -- "$(cat <&$1)" + IFS='' read -rd '' -u $1 suggestion + zle autosuggest-suggest -- "$suggestion" # Close the fd exec {1}<&-