diff --git a/Makefile b/Makefile index f6d13a7..b89ff04 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,10 @@ SRC_DIR := ./src SRC_FILES := \ + $(SRC_DIR)/setup.zsh \ $(SRC_DIR)/config.zsh \ $(SRC_DIR)/util.zsh \ + $(SRC_DIR)/features.zsh \ $(SRC_DIR)/bind.zsh \ $(SRC_DIR)/highlight.zsh \ $(SRC_DIR)/widgets.zsh \ diff --git a/README.md b/README.md index 59ee831..7e3e674 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. +It suggests commands as you type, based on command history. Requirements: Zsh v4.3.11 or later @@ -39,13 +39,10 @@ Set `ZSH_AUTOSUGGEST_HIGHLIGHT_STYLE` to configure the style that the suggestion ### Suggestion Strategy -`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: +`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 two built-in strategies to choose from: - `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. (requires `zpty` module) - -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 @@ -70,7 +67,7 @@ This can be useful when pasting large amount of text in the terminal, to avoid t ### Enable Asynchronous Mode -As of `v0.4.0`, suggestions can be fetched asynchronously. To enable this behavior, set the `ZSH_AUTOSUGGEST_USE_ASYNC` variable (it can be set to anything). +As of `v0.4.0`, suggestions can be fetched asynchronously using the `zsh/zpty` module. To enable this behavior, set the `ZSH_AUTOSUGGEST_USE_ASYNC` variable (it can be set to anything). ### Key Bindings diff --git a/spec/async_spec.rb b/spec/async_spec.rb index 9405fb2..152adde 100644 --- a/spec/async_spec.rb +++ b/spec/async_spec.rb @@ -1,4 +1,8 @@ context 'with asynchronous suggestions 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="] } describe '`up-line-or-beginning-search`' do @@ -26,6 +30,55 @@ context 'with asynchronous suggestions enabled' do end end end + + it 'should not add extra carriage returns before newlines' do + session. + send_string('echo "'). + send_keys('escape'). + send_keys('enter'). + send_string('"'). + send_keys('enter') + + session.clear_screen + + session.send_string('echo') + wait_for { session.content }.to eq("echo \"\n\"") + end + + it 'should treat carriage returns and newlines as separate characters' do + session. + send_string('echo "'). + send_keys('C-v'). + send_keys('enter'). + send_string('foo"'). + send_keys('enter') + + session. + send_string('echo "'). + send_keys('control'). + send_keys('enter'). + send_string('bar"'). + send_keys('enter') + + session.clear_screen + + session. + send_string('echo "'). + send_keys('C-v'). + send_keys('enter') + + wait_for { session.content }.to eq('echo "^Mfoo"') + end + + describe 'exiting a subshell' do + it 'should not cause error messages to be printed' do + session.run_command('$(exit)') + + sleep 1 + + expect(session.content).to eq('$(exit)') + end + end end diff --git a/spec/integrations/client_zpty_spec.rb b/spec/integrations/client_zpty_spec.rb index b8abb37..8f1550e 100644 --- a/spec/integrations/client_zpty_spec.rb +++ b/spec/integrations/client_zpty_spec.rb @@ -1,14 +1,10 @@ describe 'a running zpty command' do let(:before_sourcing) { -> { session.run_command('zmodload zsh/zpty && zpty -b kitty cat') } } - context 'when using `completion` strategy' do - let(:options) { ["ZSH_AUTOSUGGEST_STRATEGY=completion"] } + 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 $?') - 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 + wait_for { session.content }.to end_with("\n0") end end diff --git a/spec/options/async_zpty_name_spec.rb b/spec/options/async_zpty_name_spec.rb new file mode 100644 index 0000000..407ee70 --- /dev/null +++ b/spec/options/async_zpty_name_spec.rb @@ -0,0 +1,19 @@ +context 'when async suggestions are enabled' do + let(:options) { ["ZSH_AUTOSUGGEST_USE_ASYNC="] } + + 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 $?') + wait_for { session.content }.to end_with("\n0") + end + + context 'when ZSH_AUTOSUGGEST_ASYNC_PTY_NAME is set' do + let(:options) { super() + ['ZSH_AUTOSUGGEST_ASYNC_PTY_NAME=foo_pty'] } + + it 'is created with the specified name' do + session.run_command('zpty -t foo_pty &>/dev/null; echo $?') + wait_for { session.content }.to end_with("\n0") + end + end + end +end diff --git a/spec/strategies/completion_spec.rb b/spec/strategies/completion_spec.rb deleted file mode 100644 index bd2c72d..0000000 --- a/spec/strategies/completion_spec.rb +++ /dev/null @@ -1,26 +0,0 @@ -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 - 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/terminal_session.rb b/spec/terminal_session.rb index 2d9468f..f91ee6c 100644 --- a/spec/terminal_session.rb +++ b/spec/terminal_session.rb @@ -18,6 +18,10 @@ class TerminalSession tmux_command("new-session -d -x #{opts[:width]} -y #{opts[:height]} '#{cmd}'") end + def zsh_version + @zsh_version ||= Gem::Version.new(`#{ZSH_BIN} -c 'echo -n $ZSH_VERSION'`) + end + def tmux_socket_name @tmux_socket_name ||= SecureRandom.hex(6) end diff --git a/src/async.zsh b/src/async.zsh index f1877d3..dd54c24 100644 --- a/src/async.zsh +++ b/src/async.zsh @@ -3,61 +3,107 @@ # Async # #--------------------------------------------------------------------# -zmodload zsh/system +# Zpty process is spawned running this function +_zsh_autosuggest_async_server() { + emulate -R zsh -_zsh_autosuggest_async_request() { - typeset -g _ZSH_AUTOSUGGEST_ASYNC_FD _ZSH_AUTOSUGGEST_CHILD_PID + # 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 + } - # If we've got a pending request, cancel it - if [[ -n "$_ZSH_AUTOSUGGEST_ASYNC_FD" ]] && { true <&$_ZSH_AUTOSUGGEST_ASYNC_FD } 2>/dev/null; then - # Close the file descriptor and remove the handler - exec {_ZSH_AUTOSUGGEST_ASYNC_FD}<&- - zle -F $_ZSH_AUTOSUGGEST_ASYNC_FD + # Don't add any extra carriage returns + stty -onlcr - # Zsh will make a new process group for the child process only if job - # control is enabled (MONITOR option) - if [[ -o MONITOR ]]; then - # Send the signal to the process group to kill any processes that may - # have been forked by the suggestion strategy - kill -TERM -$_ZSH_AUTOSUGGEST_CHILD_PID 2>/dev/null - else - # Kill just the child process since it wasn't placed in a new process - # group. If the suggestion strategy forked any child processes they may - # be orphaned and left behind. - kill -TERM $_ZSH_AUTOSUGGEST_CHILD_PID 2>/dev/null - fi - fi + # Don't translate carriage returns to newlines + stty -icrnl - # Fork a process to fetch a suggestion and open a pipe to read from it - exec {_ZSH_AUTOSUGGEST_ASYNC_FD}< <( - # Tell parent process our pid - echo $sysparams[pid] + # Silence any error messages + exec 2>/dev/null - # Fetch and print the suggestion - local suggestion - _zsh_autosuggest_fetch_suggestion "$1" - echo -nE "$suggestion" - ) + local last_pid - # Read the pid from the child process - read _ZSH_AUTOSUGGEST_CHILD_PID <&$_ZSH_AUTOSUGGEST_ASYNC_FD + while IFS='' read -r -d $'\0' query; do + # Kill last bg process + kill -KILL $last_pid &>/dev/null - # When the fd is readable, call the response handler - zle -F "$_ZSH_AUTOSUGGEST_ASYNC_FD" _zsh_autosuggest_async_response + # Run suggestion search in the background + ( + local suggestion + _zsh_autosuggest_fetch_suggestion "$query" + echo -n -E "$suggestion"$'\0' + ) & + + last_pid=$! + done } -# Called when new data is ready to be read from the pipe +_zsh_autosuggest_async_request() { + # Write the query to the zpty process to fetch a suggestion + zpty -w -n $ZSH_AUTOSUGGEST_ASYNC_PTY_NAME "${1}"$'\0' +} + +# Called when new data is ready to be read from the pty # First arg will be fd ready for reading # Second arg will be passed in case of error _zsh_autosuggest_async_response() { - if [[ -z "$2" || "$2" == "hup" ]]; then - # Read everything from the fd and give it as a suggestion - zle autosuggest-suggest -- "$(cat <&$1)" + setopt LOCAL_OPTIONS EXTENDED_GLOB - # Close the fd - exec {1}<&- + local suggestion + + zpty -rt $ZSH_AUTOSUGGEST_ASYNC_PTY_NAME suggestion '*'$'\0' 2>/dev/null + zle autosuggest-suggest -- "${suggestion%%$'\0'##}" +} + +_zsh_autosuggest_async_pty_create() { + # With newer versions of zsh, REPLY stores the fd to read from + typeset -h REPLY + + # If we won't get a fd back from zpty, try to guess it + if (( ! $_ZSH_AUTOSUGGEST_ZPTY_RETURNS_FD )); then + integer -l zptyfd + exec {zptyfd}>&1 # Open a new file descriptor (above 10). + exec {zptyfd}>&- # Close it so it's free to be used by zpty. fi - # Always remove the handler - zle -F "$1" + # Fork a zpty process running the server function + zpty -b $ZSH_AUTOSUGGEST_ASYNC_PTY_NAME _zsh_autosuggest_async_server + + # Store the fd so we can remove the handler later + if (( REPLY )); then + _ZSH_AUTOSUGGEST_PTY_FD=$REPLY + else + _ZSH_AUTOSUGGEST_PTY_FD=$zptyfd + fi + + # Set up input handler from the zpty + zle -F $_ZSH_AUTOSUGGEST_PTY_FD _zsh_autosuggest_async_response +} + +_zsh_autosuggest_async_pty_destroy() { + # Remove the input handler + zle -F $_ZSH_AUTOSUGGEST_PTY_FD &>/dev/null + + # Destroy the zpty + zpty -d $ZSH_AUTOSUGGEST_ASYNC_PTY_NAME &>/dev/null +} + +_zsh_autosuggest_async_pty_recreate() { + _zsh_autosuggest_async_pty_destroy + _zsh_autosuggest_async_pty_create +} + +_zsh_autosuggest_async_start() { + typeset -g _ZSH_AUTOSUGGEST_PTY_FD + + _zsh_autosuggest_feature_detect_zpty_returns_fd + _zsh_autosuggest_async_pty_recreate + + # We recreate the pty to get a fresh list of history events + add-zsh-hook precmd _zsh_autosuggest_async_pty_recreate } diff --git a/src/config.zsh b/src/config.zsh index dc01b7c..9ac1484 100644 --- a/src/config.zsh +++ b/src/config.zsh @@ -69,5 +69,5 @@ # Max size of buffer to trigger autosuggestion. Leave null for no upper bound. : ${ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE=} -# Pty name for capturing completions for completion suggestion strategy -: ${ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME=zsh_autosuggest_completion_pty} +# Pty name for calculating autosuggestions asynchronously +: ${ZSH_AUTOSUGGEST_ASYNC_PTY_NAME=zsh_autosuggest_pty} diff --git a/src/features.zsh b/src/features.zsh new file mode 100644 index 0000000..7a5248f --- /dev/null +++ b/src/features.zsh @@ -0,0 +1,19 @@ + +#--------------------------------------------------------------------# +# Feature Detection # +#--------------------------------------------------------------------# + +_zsh_autosuggest_feature_detect_zpty_returns_fd() { + typeset -g _ZSH_AUTOSUGGEST_ZPTY_RETURNS_FD + typeset -h REPLY + + zpty zsh_autosuggest_feature_detect '{ zshexit() { kill -KILL $$; sleep 1 } }' + + if (( REPLY )); then + _ZSH_AUTOSUGGEST_ZPTY_RETURNS_FD=1 + else + _ZSH_AUTOSUGGEST_ZPTY_RETURNS_FD=0 + fi + + zpty -d zsh_autosuggest_feature_detect +} diff --git a/src/setup.zsh b/src/setup.zsh new file mode 100644 index 0000000..c74489f --- /dev/null +++ b/src/setup.zsh @@ -0,0 +1,10 @@ + +#--------------------------------------------------------------------# +# Setup # +#--------------------------------------------------------------------# + +# Precmd hooks for initializing the library and starting pty's +autoload -Uz add-zsh-hook + +# Asynchronous suggestions are generated in a pty +zmodload zsh/zpty diff --git a/src/start.zsh b/src/start.zsh index ff93fdf..6f48ab6 100644 --- a/src/start.zsh +++ b/src/start.zsh @@ -14,8 +14,11 @@ _zsh_autosuggest_start() { # zsh-syntax-highlighting widgets. This also allows modifications # to the widget list variables to take effect on the next precmd. add-zsh-hook precmd _zsh_autosuggest_bind_widgets + + if [[ -n "${ZSH_AUTOSUGGEST_USE_ASYNC+x}" ]]; then + _zsh_autosuggest_async_start + fi } # Start the autosuggestion widgets on the next precmd -autoload -Uz add-zsh-hook add-zsh-hook precmd _zsh_autosuggest_start diff --git a/src/strategies/completion.zsh b/src/strategies/completion.zsh deleted file mode 100644 index 7517822..0000000 --- a/src/strategies/completion.zsh +++ /dev/null @@ -1,104 +0,0 @@ - -#--------------------------------------------------------------------# -# Completion Suggestion Strategy # -#--------------------------------------------------------------------# -# Fetches a suggestion from the completion engine -# - -_zsh_autosuggest_capture_postcompletion() { - # Always insert the first completion into the buffer - compstate[insert]=1 - - # Don't list completions - unset compstate[list] -} - -_zsh_autosuggest_capture_completion_widget() { - local -a +h comppostfuncs - comppostfuncs=(_zsh_autosuggest_capture_postcompletion) - - # Only capture completions at the end of the buffer - CURSOR=$#BUFFER - - # Run the original widget wrapping `.complete-word` so we don't - # recursively try to fetch suggestions, since our pty is forked - # after autosuggestions is initialized. - zle -- ${(k)widgets[(r)completion:.complete-word:_main_complete]} - - # The completion has been added, print the buffer as the suggestion - echo -nE - $'\0'$BUFFER$'\0' -} - -zle -N autosuggest-capture-completion _zsh_autosuggest_capture_completion_widget - -_zsh_autosuggest_capture_setup() { - # 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 - # process immediately, before it has a chance to kill any other - # zpty processes. - if ! is-at-least 5.4; then - zshexit() { - kill -KILL $$ - sleep 1 # Block for long enough for the signal to come through - } - fi - - bindkey '^I' autosuggest-capture-completion -} - -_zsh_autosuggest_capture_completion_sync() { - _zsh_autosuggest_capture_setup - - zle autosuggest-capture-completion -} - -_zsh_autosuggest_capture_completion_async() { - _zsh_autosuggest_capture_setup - - zmodload zsh/parameter 2>/dev/null || return # 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 "$@" - } - - # Open zle with buffer set so we can capture completions for it - vared 1 -} - -_zsh_autosuggest_strategy_completion() { - typeset -g suggestion - local line REPLY - - # Exit if we don't have completions - whence compdef >/dev/null || return - - # Exit if we don't have zpty - zmodload zsh/zpty 2>/dev/null || return - - # Zle will be inactive if we are in async mode - if zle; then - zpty $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME _zsh_autosuggest_capture_completion_sync - else - zpty $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME _zsh_autosuggest_capture_completion_async "\$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''*'$'\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'*}" - } always { - # Destroy the pty - zpty -d $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME - } -} diff --git a/src/widgets.zsh b/src/widgets.zsh index 3312579..6a2be4a 100644 --- a/src/widgets.zsh +++ b/src/widgets.zsh @@ -95,7 +95,7 @@ _zsh_autosuggest_modify() { # Fetch a new suggestion based on what's currently in the buffer _zsh_autosuggest_fetch() { - if [[ -n "${ZSH_AUTOSUGGEST_USE_ASYNC+x}" ]]; then + if zpty -t "$ZSH_AUTOSUGGEST_ASYNC_PTY_NAME" &>/dev/null; then _zsh_autosuggest_async_request "$BUFFER" else local suggestion diff --git a/zsh-autosuggestions.zsh b/zsh-autosuggestions.zsh index 5127015..1407559 100644 --- a/zsh-autosuggestions.zsh +++ b/zsh-autosuggestions.zsh @@ -25,6 +25,16 @@ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR # OTHER DEALINGS IN THE SOFTWARE. +#--------------------------------------------------------------------# +# Setup # +#--------------------------------------------------------------------# + +# Precmd hooks for initializing the library and starting pty's +autoload -Uz add-zsh-hook + +# Asynchronous suggestions are generated in a pty +zmodload zsh/zpty + #--------------------------------------------------------------------# # Global Configuration Variables # #--------------------------------------------------------------------# @@ -95,8 +105,8 @@ # Max size of buffer to trigger autosuggestion. Leave null for no upper bound. : ${ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE=} -# Pty name for capturing completions for completion suggestion strategy -: ${ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME=zsh_autosuggest_completion_pty} +# Pty name for calculating autosuggestions asynchronously +: ${ZSH_AUTOSUGGEST_ASYNC_PTY_NAME=zsh_autosuggest_pty} #--------------------------------------------------------------------# # Utility Functions # @@ -109,6 +119,25 @@ _zsh_autosuggest_escape_command() { echo -E "${1//(#m)[\"\'\\()\[\]|*?~]/\\$MATCH}" } +#--------------------------------------------------------------------# +# Feature Detection # +#--------------------------------------------------------------------# + +_zsh_autosuggest_feature_detect_zpty_returns_fd() { + typeset -g _ZSH_AUTOSUGGEST_ZPTY_RETURNS_FD + typeset -h REPLY + + zpty zsh_autosuggest_feature_detect '{ zshexit() { kill -KILL $$; sleep 1 } }' + + if (( REPLY )); then + _ZSH_AUTOSUGGEST_ZPTY_RETURNS_FD=1 + else + _ZSH_AUTOSUGGEST_ZPTY_RETURNS_FD=0 + fi + + zpty -d zsh_autosuggest_feature_detect +} + #--------------------------------------------------------------------# # Widget Helpers # #--------------------------------------------------------------------# @@ -351,7 +380,7 @@ _zsh_autosuggest_modify() { # Fetch a new suggestion based on what's currently in the buffer _zsh_autosuggest_fetch() { - if [[ -n "${ZSH_AUTOSUGGEST_USE_ASYNC+x}" ]]; then + if zpty -t "$ZSH_AUTOSUGGEST_ASYNC_PTY_NAME" &>/dev/null; then _zsh_autosuggest_async_request "$BUFFER" else local suggestion @@ -472,110 +501,6 @@ 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 a suggestion from the completion engine -# - -_zsh_autosuggest_capture_postcompletion() { - # Always insert the first completion into the buffer - compstate[insert]=1 - - # Don't list completions - unset compstate[list] -} - -_zsh_autosuggest_capture_completion_widget() { - local -a +h comppostfuncs - comppostfuncs=(_zsh_autosuggest_capture_postcompletion) - - # Only capture completions at the end of the buffer - CURSOR=$#BUFFER - - # Run the original widget wrapping `.complete-word` so we don't - # recursively try to fetch suggestions, since our pty is forked - # after autosuggestions is initialized. - zle -- ${(k)widgets[(r)completion:.complete-word:_main_complete]} - - # The completion has been added, print the buffer as the suggestion - echo -nE - $'\0'$BUFFER$'\0' -} - -zle -N autosuggest-capture-completion _zsh_autosuggest_capture_completion_widget - -_zsh_autosuggest_capture_setup() { - # 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 - # process immediately, before it has a chance to kill any other - # zpty processes. - if ! is-at-least 5.4; then - zshexit() { - kill -KILL $$ - sleep 1 # Block for long enough for the signal to come through - } - fi - - bindkey '^I' autosuggest-capture-completion -} - -_zsh_autosuggest_capture_completion_sync() { - _zsh_autosuggest_capture_setup - - zle autosuggest-capture-completion -} - -_zsh_autosuggest_capture_completion_async() { - _zsh_autosuggest_capture_setup - - zmodload zsh/parameter 2>/dev/null || return # 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 "$@" - } - - # Open zle with buffer set so we can capture completions for it - vared 1 -} - -_zsh_autosuggest_strategy_completion() { - typeset -g suggestion - local line REPLY - - # Exit if we don't have completions - whence compdef >/dev/null || return - - # Exit if we don't have zpty - zmodload zsh/zpty 2>/dev/null || return - - # Zle will be inactive if we are in async mode - if zle; then - zpty $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME _zsh_autosuggest_capture_completion_sync - else - zpty $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME _zsh_autosuggest_capture_completion_async "\$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''*'$'\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'*}" - } always { - # Destroy the pty - zpty -d $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME - } -} - #--------------------------------------------------------------------# # History Suggestion Strategy # #--------------------------------------------------------------------# @@ -688,63 +613,109 @@ _zsh_autosuggest_fetch_suggestion() { # Async # #--------------------------------------------------------------------# -zmodload zsh/system +# Zpty process is spawned running this function +_zsh_autosuggest_async_server() { + emulate -R zsh -_zsh_autosuggest_async_request() { - typeset -g _ZSH_AUTOSUGGEST_ASYNC_FD _ZSH_AUTOSUGGEST_CHILD_PID + # 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 + } - # If we've got a pending request, cancel it - if [[ -n "$_ZSH_AUTOSUGGEST_ASYNC_FD" ]] && { true <&$_ZSH_AUTOSUGGEST_ASYNC_FD } 2>/dev/null; then - # Close the file descriptor and remove the handler - exec {_ZSH_AUTOSUGGEST_ASYNC_FD}<&- - zle -F $_ZSH_AUTOSUGGEST_ASYNC_FD + # Don't add any extra carriage returns + stty -onlcr - # Zsh will make a new process group for the child process only if job - # control is enabled (MONITOR option) - if [[ -o MONITOR ]]; then - # Send the signal to the process group to kill any processes that may - # have been forked by the suggestion strategy - kill -TERM -$_ZSH_AUTOSUGGEST_CHILD_PID 2>/dev/null - else - # Kill just the child process since it wasn't placed in a new process - # group. If the suggestion strategy forked any child processes they may - # be orphaned and left behind. - kill -TERM $_ZSH_AUTOSUGGEST_CHILD_PID 2>/dev/null - fi - fi + # Don't translate carriage returns to newlines + stty -icrnl - # Fork a process to fetch a suggestion and open a pipe to read from it - exec {_ZSH_AUTOSUGGEST_ASYNC_FD}< <( - # Tell parent process our pid - echo $sysparams[pid] + # Silence any error messages + exec 2>/dev/null - # Fetch and print the suggestion - local suggestion - _zsh_autosuggest_fetch_suggestion "$1" - echo -nE "$suggestion" - ) + local last_pid - # Read the pid from the child process - read _ZSH_AUTOSUGGEST_CHILD_PID <&$_ZSH_AUTOSUGGEST_ASYNC_FD + while IFS='' read -r -d $'\0' query; do + # Kill last bg process + kill -KILL $last_pid &>/dev/null - # When the fd is readable, call the response handler - zle -F "$_ZSH_AUTOSUGGEST_ASYNC_FD" _zsh_autosuggest_async_response + # Run suggestion search in the background + ( + local suggestion + _zsh_autosuggest_fetch_suggestion "$query" + echo -n -E "$suggestion"$'\0' + ) & + + last_pid=$! + done } -# Called when new data is ready to be read from the pipe +_zsh_autosuggest_async_request() { + # Write the query to the zpty process to fetch a suggestion + zpty -w -n $ZSH_AUTOSUGGEST_ASYNC_PTY_NAME "${1}"$'\0' +} + +# Called when new data is ready to be read from the pty # First arg will be fd ready for reading # Second arg will be passed in case of error _zsh_autosuggest_async_response() { - if [[ -z "$2" || "$2" == "hup" ]]; then - # Read everything from the fd and give it as a suggestion - zle autosuggest-suggest -- "$(cat <&$1)" + setopt LOCAL_OPTIONS EXTENDED_GLOB - # Close the fd - exec {1}<&- + local suggestion + + zpty -rt $ZSH_AUTOSUGGEST_ASYNC_PTY_NAME suggestion '*'$'\0' 2>/dev/null + zle autosuggest-suggest -- "${suggestion%%$'\0'##}" +} + +_zsh_autosuggest_async_pty_create() { + # With newer versions of zsh, REPLY stores the fd to read from + typeset -h REPLY + + # If we won't get a fd back from zpty, try to guess it + if (( ! $_ZSH_AUTOSUGGEST_ZPTY_RETURNS_FD )); then + integer -l zptyfd + exec {zptyfd}>&1 # Open a new file descriptor (above 10). + exec {zptyfd}>&- # Close it so it's free to be used by zpty. fi - # Always remove the handler - zle -F "$1" + # Fork a zpty process running the server function + zpty -b $ZSH_AUTOSUGGEST_ASYNC_PTY_NAME _zsh_autosuggest_async_server + + # Store the fd so we can remove the handler later + if (( REPLY )); then + _ZSH_AUTOSUGGEST_PTY_FD=$REPLY + else + _ZSH_AUTOSUGGEST_PTY_FD=$zptyfd + fi + + # Set up input handler from the zpty + zle -F $_ZSH_AUTOSUGGEST_PTY_FD _zsh_autosuggest_async_response +} + +_zsh_autosuggest_async_pty_destroy() { + # Remove the input handler + zle -F $_ZSH_AUTOSUGGEST_PTY_FD &>/dev/null + + # Destroy the zpty + zpty -d $ZSH_AUTOSUGGEST_ASYNC_PTY_NAME &>/dev/null +} + +_zsh_autosuggest_async_pty_recreate() { + _zsh_autosuggest_async_pty_destroy + _zsh_autosuggest_async_pty_create +} + +_zsh_autosuggest_async_start() { + typeset -g _ZSH_AUTOSUGGEST_PTY_FD + + _zsh_autosuggest_feature_detect_zpty_returns_fd + _zsh_autosuggest_async_pty_recreate + + # We recreate the pty to get a fresh list of history events + add-zsh-hook precmd _zsh_autosuggest_async_pty_recreate } #--------------------------------------------------------------------# @@ -762,8 +733,11 @@ _zsh_autosuggest_start() { # zsh-syntax-highlighting widgets. This also allows modifications # to the widget list variables to take effect on the next precmd. add-zsh-hook precmd _zsh_autosuggest_bind_widgets + + if [[ -n "${ZSH_AUTOSUGGEST_USE_ASYNC+x}" ]]; then + _zsh_autosuggest_async_start + fi } # Start the autosuggestion widgets on the next precmd -autoload -Uz add-zsh-hook add-zsh-hook precmd _zsh_autosuggest_start