From 9cb010151204926aee24e71d54879092d89c66b4 Mon Sep 17 00:00:00 2001 From: Eric Freese Date: Mon, 11 Jun 2018 02:06:18 -0600 Subject: [PATCH] Refactor async mode to no longer use zpty See technique used in `fast-syntax-highlighting`: - https://github.com/zdharma/fast-syntax-highlighting/commit/ca2e18bbc9e27b9264206c257d2ab68838162c70 - http://www.zsh.org/mla/users/2018/msg00424.html --- spec/async_spec.rb | 53 ----------- spec/integrations/client_zpty_spec.rb | 10 --- spec/options/async_zpty_name_spec.rb | 19 ---- spec/terminal_session.rb | 4 - src/async.zsh | 118 +++++------------------- src/start.zsh | 4 - src/widgets.zsh | 2 +- zsh-autosuggestions.zsh | 124 +++++--------------------- 8 files changed, 46 insertions(+), 288 deletions(-) delete mode 100644 spec/integrations/client_zpty_spec.rb delete mode 100644 spec/options/async_zpty_name_spec.rb diff --git a/spec/async_spec.rb b/spec/async_spec.rb index 152adde..9405fb2 100644 --- a/spec/async_spec.rb +++ b/spec/async_spec.rb @@ -1,8 +1,4 @@ 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 @@ -30,55 +26,6 @@ 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 deleted file mode 100644 index 8f1550e..0000000 --- a/spec/integrations/client_zpty_spec.rb +++ /dev/null @@ -1,10 +0,0 @@ -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 $?') - - 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 deleted file mode 100644 index 407ee70..0000000 --- a/spec/options/async_zpty_name_spec.rb +++ /dev/null @@ -1,19 +0,0 @@ -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/terminal_session.rb b/spec/terminal_session.rb index f91ee6c..2d9468f 100644 --- a/spec/terminal_session.rb +++ b/spec/terminal_session.rb @@ -18,10 +18,6 @@ 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 62e3329..8111496 100644 --- a/src/async.zsh +++ b/src/async.zsh @@ -3,107 +3,33 @@ # Async # #--------------------------------------------------------------------# -# Zpty process is spawned running this function -_zsh_autosuggest_async_server() { - emulate -R zsh - - # 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 - } - - # Don't add any extra carriage returns - stty -onlcr - - # Don't translate carriage returns to newlines - stty -icrnl - - # Silence any error messages - exec 2>/dev/null - - local last_pid - - while IFS='' read -r -d $'\0' query; do - # Kill last bg process - kill -KILL $last_pid &>/dev/null - - # Run suggestion search in the background - ( - local suggestion - _zsh_autosuggest_strategy_$ZSH_AUTOSUGGEST_STRATEGY "$query" - echo -n -E "$suggestion"$'\0' - ) & - - last_pid=$! - done -} - _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' + typeset -g _ZSH_AUTOSUGGEST_ASYNC_FD + + # Close the last fd to invalidate old suggestions + if [[ -n "$_ZSH_AUTOSUGGEST_ASYNC_FD" ]] && { true <&$_ZSH_AUTOSUGGEST_ASYNC_FD } 2>/dev/null; then + exec {_ZSH_AUTOSUGGEST_ASYNC_FD}<&- + fi + + # Fork a process to fetch a suggestion and open a pipe to read from it + exec {_ZSH_AUTOSUGGEST_ASYNC_FD}< <( + local suggestion + _zsh_autosuggest_fetch_suggestion "$1" + echo -nE "$suggestion" + ) + + # When the fd is readable, call the response handler + zle -F "$_ZSH_AUTOSUGGEST_ASYNC_FD" _zsh_autosuggest_async_response } -# Called when new data is ready to be read from the pty +# Called when new data is ready to be read from the pipe # First arg will be fd ready for reading # Second arg will be passed in case of error _zsh_autosuggest_async_response() { - setopt LOCAL_OPTIONS EXTENDED_GLOB + # Read everything from the fd and give it as a suggestion + zle autosuggest-suggest -- "$(<&$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 - - # 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 + # Remove the handler and close the fd + zle -F "$1" + exec {1}<&- } diff --git a/src/start.zsh b/src/start.zsh index 6f48ab6..a73ee3b 100644 --- a/src/start.zsh +++ b/src/start.zsh @@ -14,10 +14,6 @@ _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 diff --git a/src/widgets.zsh b/src/widgets.zsh index 87bb62e..4cd8ca8 100644 --- a/src/widgets.zsh +++ b/src/widgets.zsh @@ -93,7 +93,7 @@ _zsh_autosuggest_modify() { # Fetch a new suggestion based on what's currently in the buffer _zsh_autosuggest_fetch() { - if zpty -t "$ZSH_AUTOSUGGEST_ASYNC_PTY_NAME" &>/dev/null; then + if [[ -n "${ZSH_AUTOSUGGEST_USE_ASYNC+x}" ]]; then _zsh_autosuggest_async_request "$BUFFER" else local suggestion diff --git a/zsh-autosuggestions.zsh b/zsh-autosuggestions.zsh index 0a1f9c6..3106709 100644 --- a/zsh-autosuggestions.zsh +++ b/zsh-autosuggestions.zsh @@ -374,7 +374,7 @@ _zsh_autosuggest_modify() { # Fetch a new suggestion based on what's currently in the buffer _zsh_autosuggest_fetch() { - if zpty -t "$ZSH_AUTOSUGGEST_ASYNC_PTY_NAME" &>/dev/null; then + if [[ -n "${ZSH_AUTOSUGGEST_USE_ASYNC+x}" ]]; then _zsh_autosuggest_async_request "$BUFFER" else local suggestion @@ -581,109 +581,35 @@ _zsh_autosuggest_strategy_match_prev_cmd() { # Async # #--------------------------------------------------------------------# -# Zpty process is spawned running this function -_zsh_autosuggest_async_server() { - emulate -R zsh - - # 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 - } - - # Don't add any extra carriage returns - stty -onlcr - - # Don't translate carriage returns to newlines - stty -icrnl - - # Silence any error messages - exec 2>/dev/null - - local last_pid - - while IFS='' read -r -d $'\0' query; do - # Kill last bg process - kill -KILL $last_pid &>/dev/null - - # Run suggestion search in the background - ( - local suggestion - _zsh_autosuggest_strategy_$ZSH_AUTOSUGGEST_STRATEGY "$query" - echo -n -E "$suggestion"$'\0' - ) & - - last_pid=$! - done -} - _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' + typeset -g _ZSH_AUTOSUGGEST_ASYNC_FD + + # Close the last fd to invalidate old suggestions + if [[ -n "$_ZSH_AUTOSUGGEST_ASYNC_FD" ]] && { true <&$_ZSH_AUTOSUGGEST_ASYNC_FD } 2>/dev/null; then + exec {_ZSH_AUTOSUGGEST_ASYNC_FD}<&- + fi + + # Fork a process to fetch a suggestion and open a pipe to read from it + exec {_ZSH_AUTOSUGGEST_ASYNC_FD}< <( + local suggestion + _zsh_autosuggest_fetch_suggestion "$1" + echo -nE "$suggestion" + ) + + # When the fd is readable, call the response handler + zle -F "$_ZSH_AUTOSUGGEST_ASYNC_FD" _zsh_autosuggest_async_response } -# Called when new data is ready to be read from the pty +# Called when new data is ready to be read from the pipe # First arg will be fd ready for reading # Second arg will be passed in case of error _zsh_autosuggest_async_response() { - setopt LOCAL_OPTIONS EXTENDED_GLOB + # Read everything from the fd and give it as a suggestion + zle autosuggest-suggest -- "$(<&$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 - - # 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 + # Remove the handler and close the fd + zle -F "$1" + exec {1}<&- } #--------------------------------------------------------------------# @@ -701,10 +627,6 @@ _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