# Fish-like fast/unobtrusive autosuggestions for zsh. # https://github.com/zsh-users/zsh-autosuggestions # v0.4.3 # Copyright (c) 2013 Thiago de Arruda # Copyright (c) 2016-2018 Eric Freese # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation # files (the "Software"), to deal in the Software without # restriction, including without limitation the rights to use, # copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the # Software is furnished to do so, subject to the following # conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # 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 # #--------------------------------------------------------------------# # Color to use when highlighting suggestion # Uses format of `region_highlight` # More info: http://zsh.sourceforge.net/Doc/Release/Zsh-Line-Editor.html#Zle-Widgets 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 # Widgets that clear the suggestion ZSH_AUTOSUGGEST_CLEAR_WIDGETS=( history-search-forward history-search-backward history-beginning-search-forward history-beginning-search-backward history-substring-search-up history-substring-search-down up-line-or-beginning-search down-line-or-beginning-search up-line-or-history down-line-or-history accept-line ) # Widgets that accept the entire suggestion ZSH_AUTOSUGGEST_ACCEPT_WIDGETS=( forward-char end-of-line vi-forward-char vi-end-of-line vi-add-eol ) # Widgets that accept the entire suggestion and execute it ZSH_AUTOSUGGEST_EXECUTE_WIDGETS=( ) # Widgets that accept the suggestion as far as the cursor moves ZSH_AUTOSUGGEST_PARTIAL_ACCEPT_WIDGETS=( forward-word emacs-forward-word vi-forward-word vi-forward-word-end vi-forward-blank-word vi-forward-blank-word-end vi-find-next-char vi-find-next-char-skip ) # Widgets that should be ignored (globbing supported but must be escaped) ZSH_AUTOSUGGEST_IGNORE_WIDGETS=( orig-\* beep run-help set-local-history which-command yank yank-pop ) # Max size of buffer to trigger autosuggestion. Leave undefined for no upper bound. ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE= # Pty name for calculating autosuggestions asynchronously ZSH_AUTOSUGGEST_ASYNC_PTY_NAME=zsh_autosuggest_pty #--------------------------------------------------------------------# # Utility Functions # #--------------------------------------------------------------------# _zsh_autosuggest_escape_command() { setopt localoptions EXTENDED_GLOB # Escape special chars in the string (requires EXTENDED_GLOB) 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 # #--------------------------------------------------------------------# _zsh_autosuggest_incr_bind_count() { if ((${+_ZSH_AUTOSUGGEST_BIND_COUNTS[$1]})); then ((_ZSH_AUTOSUGGEST_BIND_COUNTS[$1]++)) else _ZSH_AUTOSUGGEST_BIND_COUNTS[$1]=1 fi typeset -gi bind_count=$_ZSH_AUTOSUGGEST_BIND_COUNTS[$1] } _zsh_autosuggest_get_bind_count() { if ((${+_ZSH_AUTOSUGGEST_BIND_COUNTS[$1]})); then typeset -gi bind_count=$_ZSH_AUTOSUGGEST_BIND_COUNTS[$1] else typeset -gi bind_count=0 fi } # Bind a single widget to an autosuggest widget, saving a reference to the original widget _zsh_autosuggest_bind_widget() { typeset -gA _ZSH_AUTOSUGGEST_BIND_COUNTS local widget=$1 local autosuggest_action=$2 local prefix=$ZSH_AUTOSUGGEST_ORIGINAL_WIDGET_PREFIX local -i bind_count # Save a reference to the original widget case $widgets[$widget] in # Already bound user:_zsh_autosuggest_(bound|orig)_*);; # User-defined widget user:*) _zsh_autosuggest_incr_bind_count $widget zle -N $prefix${bind_count}-$widget ${widgets[$widget]#*:} ;; # Built-in widget builtin) _zsh_autosuggest_incr_bind_count $widget eval "_zsh_autosuggest_orig_${(q)widget}() { zle .${(q)widget} }" zle -N $prefix${bind_count}-$widget _zsh_autosuggest_orig_$widget ;; # Completion widget completion:*) _zsh_autosuggest_incr_bind_count $widget eval "zle -C $prefix${bind_count}-${(q)widget} ${${(s.:.)widgets[$widget]}[2,3]}" ;; esac _zsh_autosuggest_get_bind_count $widget # Pass the original widget's name explicitly into the autosuggest # function. Use this passed in widget name to call the original # widget instead of relying on the $WIDGET variable being set # correctly. $WIDGET cannot be trusted because other plugins call # zle without the `-w` flag (e.g. `zle self-insert` instead of # `zle self-insert -w`). eval "_zsh_autosuggest_bound_${bind_count}_${(q)widget}() { _zsh_autosuggest_widget_$autosuggest_action $prefix$bind_count-${(q)widget} \$@ }" # Create the bound widget zle -N $widget _zsh_autosuggest_bound_${bind_count}_$widget } # Map all configured widgets to the right autosuggest widgets _zsh_autosuggest_bind_widgets() { local widget local ignore_widgets ignore_widgets=( .\* _\* zle-\* autosuggest-\* $ZSH_AUTOSUGGEST_ORIGINAL_WIDGET_PREFIX\* $ZSH_AUTOSUGGEST_IGNORE_WIDGETS ) # Find every widget we might want to bind and bind it appropriately for widget in ${${(f)"$(builtin zle -la)"}:#${(j:|:)~ignore_widgets}}; do if [[ -n ${ZSH_AUTOSUGGEST_CLEAR_WIDGETS[(r)$widget]} ]]; then _zsh_autosuggest_bind_widget $widget clear elif [[ -n ${ZSH_AUTOSUGGEST_ACCEPT_WIDGETS[(r)$widget]} ]]; then _zsh_autosuggest_bind_widget $widget accept elif [[ -n ${ZSH_AUTOSUGGEST_EXECUTE_WIDGETS[(r)$widget]} ]]; then _zsh_autosuggest_bind_widget $widget execute elif [[ -n ${ZSH_AUTOSUGGEST_PARTIAL_ACCEPT_WIDGETS[(r)$widget]} ]]; then _zsh_autosuggest_bind_widget $widget partial_accept else # Assume any unspecified widget might modify the buffer _zsh_autosuggest_bind_widget $widget modify fi done } # Given the name of an original widget and args, invoke it, if it exists _zsh_autosuggest_invoke_original_widget() { # Do nothing unless called with at least one arg (( $# )) || return 0 local original_widget_name="$1" shift if (( ${+widgets[$original_widget_name]} )); then zle $original_widget_name -- $@ fi } #--------------------------------------------------------------------# # Highlighting # #--------------------------------------------------------------------# # If there was a highlight, remove it _zsh_autosuggest_highlight_reset() { typeset -g _ZSH_AUTOSUGGEST_LAST_HIGHLIGHT if [[ -n "$_ZSH_AUTOSUGGEST_LAST_HIGHLIGHT" ]]; then region_highlight=("${(@)region_highlight:#$_ZSH_AUTOSUGGEST_LAST_HIGHLIGHT}") unset _ZSH_AUTOSUGGEST_LAST_HIGHLIGHT fi } # If there's a suggestion, highlight it _zsh_autosuggest_highlight_apply() { typeset -g _ZSH_AUTOSUGGEST_LAST_HIGHLIGHT if (( $#POSTDISPLAY )); then typeset -g _ZSH_AUTOSUGGEST_LAST_HIGHLIGHT="$#BUFFER $(($#BUFFER + $#POSTDISPLAY)) $ZSH_AUTOSUGGEST_HIGHLIGHT_STYLE" region_highlight+=("$_ZSH_AUTOSUGGEST_LAST_HIGHLIGHT") else unset _ZSH_AUTOSUGGEST_LAST_HIGHLIGHT fi } #--------------------------------------------------------------------# # Autosuggest Widget Implementations # #--------------------------------------------------------------------# # Disable suggestions _zsh_autosuggest_disable() { typeset -g _ZSH_AUTOSUGGEST_DISABLED _zsh_autosuggest_clear } # Enable suggestions _zsh_autosuggest_enable() { unset _ZSH_AUTOSUGGEST_DISABLED if (( $#BUFFER )); then _zsh_autosuggest_fetch fi } # Toggle suggestions (enable/disable) _zsh_autosuggest_toggle() { if [[ -n "${_ZSH_AUTOSUGGEST_DISABLED+x}" ]]; then _zsh_autosuggest_enable else _zsh_autosuggest_disable fi } # Clear the suggestion _zsh_autosuggest_clear() { # Remove the suggestion unset POSTDISPLAY _zsh_autosuggest_invoke_original_widget $@ } # Modify the buffer and get a new suggestion _zsh_autosuggest_modify() { local -i retval # Only available in zsh >= 5.4 local -i KEYS_QUEUED_COUNT # Save the contents of the buffer/postdisplay local orig_buffer="$BUFFER" local orig_postdisplay="$POSTDISPLAY" # Clear suggestion while waiting for next one unset POSTDISPLAY # Original widget may modify the buffer _zsh_autosuggest_invoke_original_widget $@ retval=$? # Don't fetch a new suggestion if there's more input to be read immediately if (( $PENDING > 0 )) || (( $KEYS_QUEUED_COUNT > 0 )); then POSTDISPLAY="$orig_postdisplay" return $retval fi # Optimize if manually typing in the suggestion if (( $#BUFFER > $#orig_buffer )); then local added=${BUFFER#$orig_buffer} # If the string added matches the beginning of the postdisplay if [[ "$added" = "${orig_postdisplay:0:$#added}" ]]; then POSTDISPLAY="${orig_postdisplay:$#added}" return $retval fi fi # Don't fetch a new suggestion if the buffer hasn't changed if [[ "$BUFFER" = "$orig_buffer" ]]; then POSTDISPLAY="$orig_postdisplay" return $retval fi # Bail out if suggestions are disabled if [[ -n "${_ZSH_AUTOSUGGEST_DISABLED+x}" ]]; then return $? fi # Get a new suggestion if the buffer is not empty after modification if (( $#BUFFER > 0 )); then if [[ -z "$ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE" ]] || (( $#BUFFER <= $ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE )); then _zsh_autosuggest_fetch fi fi return $retval } # 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 _zsh_autosuggest_async_request "$BUFFER" else local suggestion _zsh_autosuggest_strategy_$ZSH_AUTOSUGGEST_STRATEGY "$BUFFER" _zsh_autosuggest_suggest "$suggestion" fi } # Offer a suggestion _zsh_autosuggest_suggest() { local suggestion="$1" if [[ -n "$suggestion" ]] && (( $#BUFFER )); then POSTDISPLAY="${suggestion#$BUFFER}" else unset POSTDISPLAY fi } # Accept the entire suggestion _zsh_autosuggest_accept() { local -i max_cursor_pos=$#BUFFER # When vicmd keymap is active, the cursor can't move all the way # to the end of the buffer if [[ "$KEYMAP" = "vicmd" ]]; then max_cursor_pos=$((max_cursor_pos - 1)) fi # Only accept if the cursor is at the end of the buffer if [[ $CURSOR = $max_cursor_pos ]]; then # Add the suggestion to the buffer BUFFER="$BUFFER$POSTDISPLAY" # Remove the suggestion unset POSTDISPLAY # Move the cursor to the end of the buffer CURSOR=${#BUFFER} fi _zsh_autosuggest_invoke_original_widget $@ } # Accept the entire suggestion and execute it _zsh_autosuggest_execute() { # Add the suggestion to the buffer BUFFER="$BUFFER$POSTDISPLAY" # Remove the suggestion unset POSTDISPLAY # Call the original `accept-line` to handle syntax highlighting or # other potential custom behavior _zsh_autosuggest_invoke_original_widget "accept-line" } # Partially accept the suggestion _zsh_autosuggest_partial_accept() { local -i retval cursor_loc # Save the contents of the buffer so we can restore later if needed local original_buffer="$BUFFER" # Temporarily accept the suggestion. BUFFER="$BUFFER$POSTDISPLAY" # Original widget moves the cursor _zsh_autosuggest_invoke_original_widget $@ retval=$? # Normalize cursor location across vi/emacs modes cursor_loc=$CURSOR if [[ "$KEYMAP" = "vicmd" ]]; then cursor_loc=$((cursor_loc + 1)) fi # If we've moved past the end of the original buffer if (( $cursor_loc > $#original_buffer )); then # Set POSTDISPLAY to text right of the cursor POSTDISPLAY="${BUFFER[$(($cursor_loc + 1)),$#BUFFER]}" # Clip the buffer at the cursor BUFFER="${BUFFER[1,$cursor_loc]}" else # Restore the original buffer BUFFER="$original_buffer" fi return $retval } for action in clear modify fetch suggest accept partial_accept execute enable disable toggle; do eval "_zsh_autosuggest_widget_$action() { local -i retval _zsh_autosuggest_highlight_reset _zsh_autosuggest_$action \$@ retval=\$? _zsh_autosuggest_highlight_apply zle -R return \$retval }" done zle -N autosuggest-fetch _zsh_autosuggest_widget_fetch zle -N autosuggest-suggest _zsh_autosuggest_widget_suggest zle -N autosuggest-accept _zsh_autosuggest_widget_accept zle -N autosuggest-clear _zsh_autosuggest_widget_clear zle -N autosuggest-execute _zsh_autosuggest_widget_execute zle -N autosuggest-enable _zsh_autosuggest_widget_enable zle -N autosuggest-disable _zsh_autosuggest_widget_disable zle -N autosuggest-toggle _zsh_autosuggest_widget_toggle #--------------------------------------------------------------------# # Default Suggestion Strategy # #--------------------------------------------------------------------# # Suggests the most recent history item that matches the given # prefix. # _zsh_autosuggest_strategy_default() { # 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}*]}" } #--------------------------------------------------------------------# # Match Previous Command Suggestion Strategy # #--------------------------------------------------------------------# # Suggests the most recent history item that matches the given # prefix and whose preceding history item also matches the most # recently executed command. # # For example, suppose your history has the following entries: # - pwd # - ls foo # - ls bar # - pwd # # Given the history list above, when you type 'ls', the suggestion # will be 'ls foo' rather than 'ls bar' because your most recently # executed command (pwd) was previously followed by 'ls foo'. # # 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`. _zsh_autosuggest_strategy_match_prev_cmd() { # Reset options to defaults and enable LOCAL_OPTIONS emulate -L zsh # Enable globbing flags so that we can use (#m) setopt EXTENDED_GLOB # TODO: Use (b) flag when we can drop support for zsh older than v5.0.8 local prefix="${1//(#m)[\\*?[\]<>()|^~#]/\\$MATCH}" # Get all history event numbers that correspond to history # entries that match pattern $prefix* local history_match_keys history_match_keys=(${(k)history[(R)$prefix*]}) # By default we use the first history number (most recent history entry) local histkey="${history_match_keys[1]}" # Get the previously executed command local prev_cmd="$(_zsh_autosuggest_escape_command "${history[$((HISTCMD-1))]}")" # Iterate up to the first 200 history event numbers that match $prefix for key in "${(@)history_match_keys[1,200]}"; do # Stop if we ran out of history [[ $key -gt 1 ]] || break # See if the history entry preceding the suggestion matches the # previous command, and use it if it does if [[ "${history[$((key - 1))]}" == "$prev_cmd" ]]; then histkey="$key" break fi done # Give back the matched history entry typeset -g suggestion="$history[$histkey]" } #--------------------------------------------------------------------# # 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 } # Output only newlines (not carriage return + newline) stty -onlcr # 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' } # 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() { setopt LOCAL_OPTIONS EXTENDED_GLOB 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 } #--------------------------------------------------------------------# # Start # #--------------------------------------------------------------------# # Start the autosuggestion widgets _zsh_autosuggest_start() { add-zsh-hook -d precmd _zsh_autosuggest_start _zsh_autosuggest_bind_widgets # Re-bind widgets on every precmd to ensure we wrap other wrappers. # Specifically, highlighting breaks if our widgets are wrapped by # 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 add-zsh-hook precmd _zsh_autosuggest_start