diff --git a/Makefile b/Makefile index 27d02db..2ea2763 100644 --- a/Makefile +++ b/Makefile @@ -2,13 +2,15 @@ SRC_DIR := ./src VENDOR_DIR := ./vendor SRC_FILES := \ + $(SRC_DIR)/setup.zsh \ $(SRC_DIR)/config.zsh \ + $(SRC_DIR)/util.zsh \ $(SRC_DIR)/deprecated.zsh \ $(SRC_DIR)/bind.zsh \ $(SRC_DIR)/highlight.zsh \ $(SRC_DIR)/widgets.zsh \ - $(SRC_DIR)/suggestion.zsh \ $(SRC_DIR)/strategies/*.zsh \ + $(SRC_DIR)/async.zsh \ $(SRC_DIR)/start.zsh HEADER_FILES := \ diff --git a/src/async.zsh b/src/async.zsh new file mode 100644 index 0000000..a87fd05 --- /dev/null +++ b/src/async.zsh @@ -0,0 +1,53 @@ + +#--------------------------------------------------------------------# +# Async # +#--------------------------------------------------------------------# + +_zsh_autosuggest_async_fetch_suggestion() { + local strategy_function="_zsh_autosuggest_strategy_$ZSH_AUTOSUGGEST_STRATEGY" + local prefix="$(_zsh_autosuggest_escape_command "$1")" + + # Send the suggestion command to the pty to fetch a suggestion + zpty -w -n $ZSH_AUTOSUGGEST_PTY_NAME "$strategy_function '$prefix'"$'\0' +} + +_zsh_autosuggest_async_suggestion_worker() { + local last_pid + + while read -d $'\0' cmd; do + # Kill last bg process + kill -KILL $last_pid &>/dev/null + + # Run suggestion search in the background + print -n -- "$(eval "$cmd")"$'\0' & + + # Save the bg process's id so we can kill later + last_pid=$! + done +} + +_zsh_autosuggest_async_suggestion_ready() { + # while zpty -rt $ZSH_AUTOSUGGEST_PTY_NAME suggestion 2>/dev/null; do + while read -u $_ZSH_AUTOSUGGEST_PTY_FD -d $'\0' suggestion; do + zle _autosuggest-show-suggestion "${suggestion//$'\r'$'\n'/$'\n'}" + done +} + +# Recreate the pty to get a fresh list of history events +_zsh_autosuggest_async_recreate_pty() { + typeset -g _ZSH_AUTOSUGGEST_PTY_FD + + # Kill the old pty + if [ -n "$_ZSH_AUTOSUGGEST_PTY_FD" ]; then + zle -F $_ZSH_AUTOSUGGEST_PTY_FD + zpty -d $ZSH_AUTOSUGGEST_PTY_NAME &>/dev/null + fi + + # Start a new pty + typeset -h REPLY + zpty -b $ZSH_AUTOSUGGEST_PTY_NAME _zsh_autosuggest_async_suggestion_worker + _ZSH_AUTOSUGGEST_PTY_FD=$REPLY + zle -F $_ZSH_AUTOSUGGEST_PTY_FD _zsh_autosuggest_async_suggestion_ready +} + +add-zsh-hook precmd _zsh_autosuggest_async_recreate_pty diff --git a/src/config.zsh b/src/config.zsh index f519f6f..68d3b32 100644 --- a/src/config.zsh +++ b/src/config.zsh @@ -11,6 +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- +# Pty name for calculating autosuggestions asynchronously +ZSH_AUTOSUGGEST_PTY_NAME=zsh_autosuggest_pty + ZSH_AUTOSUGGEST_STRATEGY=default # Widgets that clear the suggestion 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 54f5bb8..5314e1f 100644 --- a/src/start.zsh +++ b/src/start.zsh @@ -9,5 +9,4 @@ _zsh_autosuggest_start() { _zsh_autosuggest_bind_widgets } -autoload -Uz add-zsh-hook add-zsh-hook precmd _zsh_autosuggest_start diff --git a/src/suggestion.zsh b/src/suggestion.zsh deleted file mode 100644 index 31a9f76..0000000 --- a/src/suggestion.zsh +++ /dev/null @@ -1,21 +0,0 @@ - -#--------------------------------------------------------------------# -# Suggestion # -#--------------------------------------------------------------------# - -# Delegate to the selected strategy to determine a suggestion -_zsh_autosuggest_suggestion() { - local escaped_prefix="$(_zsh_autosuggest_escape_command "$1")" - local strategy_function="_zsh_autosuggest_strategy_$ZSH_AUTOSUGGEST_STRATEGY" - - if [ -n "$functions[$strategy_function]" ]; then - echo -E "$($strategy_function "$escaped_prefix")" - fi -} - -_zsh_autosuggest_escape_command() { - setopt localoptions EXTENDED_GLOB - - # Escape special chars in the string (requires EXTENDED_GLOB) - echo -E "${1//(#m)[\\()\[\]|*?~]/\\$MATCH}" -} diff --git a/src/util.zsh b/src/util.zsh new file mode 100644 index 0000000..1f55d36 --- /dev/null +++ b/src/util.zsh @@ -0,0 +1,11 @@ + +#--------------------------------------------------------------------# +# Utility Functions # +#--------------------------------------------------------------------# + +_zsh_autosuggest_escape_command() { + setopt localoptions EXTENDED_GLOB + + # Escape special chars in the string (requires EXTENDED_GLOB) + echo -E "${1//(#m)[\"\'\\()\[\]|*?~]/\\$MATCH}" +} diff --git a/src/widgets.zsh b/src/widgets.zsh index 57f378e..0dfb3f0 100644 --- a/src/widgets.zsh +++ b/src/widgets.zsh @@ -19,7 +19,7 @@ _zsh_autosuggest_modify() { local orig_buffer="$BUFFER" local orig_postdisplay="$POSTDISPLAY" - # Clear suggestion while original widget runs + # Clear suggestion while waiting for next one unset POSTDISPLAY # Original widget may modify the buffer @@ -33,18 +33,12 @@ _zsh_autosuggest_modify() { fi # Get a new suggestion if the buffer is not empty after modification - local suggestion if [ $#BUFFER -gt 0 ]; then if [ -z "$ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE" -o $#BUFFER -lt "$ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE" ]; then - suggestion="$(_zsh_autosuggest_suggestion "$BUFFER")" + _zsh_autosuggest_async_fetch_suggestion "$BUFFER" fi fi - # Add the suggestion to the POSTDISPLAY - if [ -n "$suggestion" ]; then - POSTDISPLAY="${suggestion#$BUFFER}" - fi - return $retval } @@ -133,3 +127,21 @@ done zle -N autosuggest-accept _zsh_autosuggest_widget_accept zle -N autosuggest-clear _zsh_autosuggest_widget_clear zle -N autosuggest-execute _zsh_autosuggest_widget_execute + +_zsh_autosuggest_show_suggestion() { + local suggestion=$1 + + _zsh_autosuggest_highlight_reset + + if [ -n "$suggestion" ]; then + POSTDISPLAY="${suggestion#$BUFFER}" + else + unset POSTDISPLAY + fi + + _zsh_autosuggest_highlight_apply + + zle -R +} + +zle -N _autosuggest-show-suggestion _zsh_autosuggest_show_suggestion diff --git a/zsh-autosuggestions.zsh b/zsh-autosuggestions.zsh index 3761efe..34d7771 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 # #--------------------------------------------------------------------# @@ -37,6 +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- +# Pty name for calculating autosuggestions asynchronously +ZSH_AUTOSUGGEST_PTY_NAME=zsh_autosuggest_pty + ZSH_AUTOSUGGEST_STRATEGY=default # Widgets that clear the suggestion @@ -87,6 +100,17 @@ ZSH_AUTOSUGGEST_IGNORE_WIDGETS=( # Max size of buffer to trigger autosuggestion. Leave undefined for no upper bound. ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE= +#--------------------------------------------------------------------# +# Utility Functions # +#--------------------------------------------------------------------# + +_zsh_autosuggest_escape_command() { + setopt localoptions EXTENDED_GLOB + + # Escape special chars in the string (requires EXTENDED_GLOB) + echo -E "${1//(#m)[\"\'\\()\[\]|*?~]/\\$MATCH}" +} + #--------------------------------------------------------------------# # Handle Deprecated Variables/Widgets # #--------------------------------------------------------------------# @@ -260,7 +284,7 @@ _zsh_autosuggest_modify() { local orig_buffer="$BUFFER" local orig_postdisplay="$POSTDISPLAY" - # Clear suggestion while original widget runs + # Clear suggestion while waiting for next one unset POSTDISPLAY # Original widget may modify the buffer @@ -274,18 +298,12 @@ _zsh_autosuggest_modify() { fi # Get a new suggestion if the buffer is not empty after modification - local suggestion if [ $#BUFFER -gt 0 ]; then if [ -z "$ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE" -o $#BUFFER -lt "$ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE" ]; then - suggestion="$(_zsh_autosuggest_suggestion "$BUFFER")" + _zsh_autosuggest_async_fetch_suggestion "$BUFFER" fi fi - # Add the suggestion to the POSTDISPLAY - if [ -n "$suggestion" ]; then - POSTDISPLAY="${suggestion#$BUFFER}" - fi - return $retval } @@ -375,26 +393,23 @@ zle -N autosuggest-accept _zsh_autosuggest_widget_accept zle -N autosuggest-clear _zsh_autosuggest_widget_clear zle -N autosuggest-execute _zsh_autosuggest_widget_execute -#--------------------------------------------------------------------# -# Suggestion # -#--------------------------------------------------------------------# +_zsh_autosuggest_show_suggestion() { + local suggestion=$1 -# Delegate to the selected strategy to determine a suggestion -_zsh_autosuggest_suggestion() { - local escaped_prefix="$(_zsh_autosuggest_escape_command "$1")" - local strategy_function="_zsh_autosuggest_strategy_$ZSH_AUTOSUGGEST_STRATEGY" + _zsh_autosuggest_highlight_reset - if [ -n "$functions[$strategy_function]" ]; then - echo -E "$($strategy_function "$escaped_prefix")" + if [ -n "$suggestion" ]; then + POSTDISPLAY="${suggestion#$BUFFER}" + else + unset POSTDISPLAY fi + + _zsh_autosuggest_highlight_apply + + zle -R } -_zsh_autosuggest_escape_command() { - setopt localoptions EXTENDED_GLOB - - # Escape special chars in the string (requires EXTENDED_GLOB) - echo -E "${1//(#m)[\\()\[\]|*?~]/\\$MATCH}" -} +zle -N _autosuggest-show-suggestion _zsh_autosuggest_show_suggestion #--------------------------------------------------------------------# # Default Suggestion Strategy # @@ -459,6 +474,59 @@ _zsh_autosuggest_strategy_match_prev_cmd() { echo -E "$history[$histkey]" } +#--------------------------------------------------------------------# +# Async # +#--------------------------------------------------------------------# + +_zsh_autosuggest_async_fetch_suggestion() { + local strategy_function="_zsh_autosuggest_strategy_$ZSH_AUTOSUGGEST_STRATEGY" + local prefix="$(_zsh_autosuggest_escape_command "$1")" + + # Send the suggestion command to the pty to fetch a suggestion + zpty -w -n $ZSH_AUTOSUGGEST_PTY_NAME "$strategy_function '$prefix'"$'\0' +} + +_zsh_autosuggest_async_suggestion_worker() { + local last_pid + + while read -d $'\0' cmd; do + # Kill last bg process + kill -KILL $last_pid &>/dev/null + + # Run suggestion search in the background + print -n -- "$(eval "$cmd")"$'\0' & + + # Save the bg process's id so we can kill later + last_pid=$! + done +} + +_zsh_autosuggest_async_suggestion_ready() { + # while zpty -rt $ZSH_AUTOSUGGEST_PTY_NAME suggestion 2>/dev/null; do + while read -u $_ZSH_AUTOSUGGEST_PTY_FD -d $'\0' suggestion; do + zle _autosuggest-show-suggestion "${suggestion//$'\r'$'\n'/$'\n'}" + done +} + +# Recreate the pty to get a fresh list of history events +_zsh_autosuggest_async_recreate_pty() { + typeset -g _ZSH_AUTOSUGGEST_PTY_FD + + # Kill the old pty + if [ -n "$_ZSH_AUTOSUGGEST_PTY_FD" ]; then + zle -F $_ZSH_AUTOSUGGEST_PTY_FD + zpty -d $ZSH_AUTOSUGGEST_PTY_NAME &>/dev/null + fi + + # Start a new pty + typeset -h REPLY + zpty -b $ZSH_AUTOSUGGEST_PTY_NAME _zsh_autosuggest_async_suggestion_worker + _ZSH_AUTOSUGGEST_PTY_FD=$REPLY + zle -F $_ZSH_AUTOSUGGEST_PTY_FD _zsh_autosuggest_async_suggestion_ready +} + +add-zsh-hook precmd _zsh_autosuggest_async_recreate_pty + #--------------------------------------------------------------------# # Start # #--------------------------------------------------------------------# @@ -469,5 +537,4 @@ _zsh_autosuggest_start() { _zsh_autosuggest_bind_widgets } -autoload -Uz add-zsh-hook add-zsh-hook precmd _zsh_autosuggest_start