# Fish-like fast/unobtrusive autosuggestions for zsh. # https://github.com/zsh-users/zsh-autosuggestions # v0.7.1 # Copyright (c) 2013 Thiago de Arruda # Copyright (c) 2016-2021 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. #--------------------------------------------------------------------# # 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} )) && typeset -g ZSH_AUTOSUGGEST_HIGHLIGHT_STYLE='fg=8' # Prefix to use when saving original versions of bound widgets (( ! ${+ZSH_AUTOSUGGEST_ORIGINAL_WIDGET_PREFIX} )) && typeset -g ZSH_AUTOSUGGEST_ORIGINAL_WIDGET_PREFIX=autosuggest-orig- # Strategies to use to fetch a suggestion # Will try each strategy in order until a suggestion is returned (( ! ${+ZSH_AUTOSUGGEST_STRATEGY} )) && { typeset -ga ZSH_AUTOSUGGEST_STRATEGY ZSH_AUTOSUGGEST_STRATEGY=(history) } # Widgets that clear the suggestion (( ! ${+ZSH_AUTOSUGGEST_CLEAR_WIDGETS} )) && { typeset -ga ZSH_AUTOSUGGEST_CLEAR_WIDGETS ZSH_AUTOSUGGEST_CLEAR_WIDGETS=( history-search-forward history-search-backward history-beginning-search-forward history-beginning-search-backward history-beginning-search-forward-end history-beginning-search-backward-end 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 copy-earlier-word ) } # Widgets that accept the entire suggestion (( ! ${+ZSH_AUTOSUGGEST_ACCEPT_WIDGETS} )) && { typeset -ga ZSH_AUTOSUGGEST_ACCEPT_WIDGETS 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} )) && { typeset -ga ZSH_AUTOSUGGEST_EXECUTE_WIDGETS ZSH_AUTOSUGGEST_EXECUTE_WIDGETS=( ) } # Widgets that accept the suggestion as far as the cursor moves (( ! ${+ZSH_AUTOSUGGEST_PARTIAL_ACCEPT_WIDGETS} )) && { typeset -ga ZSH_AUTOSUGGEST_PARTIAL_ACCEPT_WIDGETS 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} )) && { typeset -ga ZSH_AUTOSUGGEST_IGNORE_WIDGETS ZSH_AUTOSUGGEST_IGNORE_WIDGETS=( orig-\* beep run-help set-local-history which-command yank yank-pop zle-\* ) } # Pty name for capturing completions for completion suggestion strategy (( ! ${+ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME} )) && typeset -g ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME=zsh_autosuggest_completion_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}" } #--------------------------------------------------------------------# # Widget Helpers # #--------------------------------------------------------------------# _zsh_autosuggest_incr_bind_count() { typeset -gi bind_count=$((_ZSH_AUTOSUGGEST_BIND_COUNTS[$1]+1)) _ZSH_AUTOSUGGEST_BIND_COUNTS[$1]=$bind_count } # 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)_*) bind_count=$((_ZSH_AUTOSUGGEST_BIND_COUNTS[$widget])) ;; # 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 # 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() { emulate -L zsh local widget local ignore_widgets ignore_widgets=( .\* _\* ${_ZSH_AUTOSUGGEST_BUILTIN_ACTIONS/#/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 # #--------------------------------------------------------------------# typeset -gi _ZSH_AUTOSUGGEST_SUGGESTION_INDEX=0 typeset -gi _ZSH_AUTOSUGGEST_PENDING_OFFSET=0 typeset -g _ZSH_AUTOSUGGEST_PENDING_BUFFER typeset -g _ZSH_AUTOSUGGEST_LAST_PREFIX # 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 } _zsh_autosuggest_next() { emulate -L zsh # Do nothing if suggestions are disabled or there's no buffer to base suggestions on if (( ${+_ZSH_AUTOSUGGEST_DISABLED} )) || (( $#BUFFER == 0 )); then return 0 fi local -i current_index=${_ZSH_AUTOSUGGEST_SUGGESTION_INDEX:-0} local -i next_index=$((current_index + 1)) # Fetch synchronously to avoid races _zsh_autosuggest_fetch $next_index "sync" # If no suggestion at next_index, wrap back to 0 if (( _ZSH_AUTOSUGGEST_SUGGESTION_INDEX != next_index )); then _zsh_autosuggest_fetch 0 "sync" fi return 0 } _zsh_autosuggest_previous() { emulate -L zsh if (( ${+_ZSH_AUTOSUGGEST_DISABLED} )) || (( $#BUFFER == 0 )); then return 0 fi local -i current_index=${_ZSH_AUTOSUGGEST_SUGGESTION_INDEX:-0} if (( current_index > 0 )); then local -i prev_index=$((current_index - 1)) _zsh_autosuggest_fetch $prev_index "sync" return 0 fi # We are at the first suggestion; step forward until we can't to find the last entry. _zsh_autosuggest_fetch 0 "sync" if [[ -z "$POSTDISPLAY" ]]; then return 0 fi local -i probe=0 local -i last_index=0 while true; do (( probe++ )) _zsh_autosuggest_fetch $probe "sync" if (( _ZSH_AUTOSUGGEST_SUGGESTION_INDEX == probe )); then last_index=$probe continue fi break done _zsh_autosuggest_fetch $last_index "sync" return 0 } # Toggle suggestions (enable/disable) _zsh_autosuggest_toggle() { if (( ${+_ZSH_AUTOSUGGEST_DISABLED} )); then _zsh_autosuggest_enable else _zsh_autosuggest_disable fi } # Clear the suggestion _zsh_autosuggest_clear() { # Remove the suggestion POSTDISPLAY= _ZSH_AUTOSUGGEST_SUGGESTION_INDEX=0 _ZSH_AUTOSUGGEST_LAST_PREFIX="$BUFFER" _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 POSTDISPLAY= # Original widget may modify the buffer _zsh_autosuggest_invoke_original_widget $@ retval=$? emulate -L zsh # 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 or if buffer hasn't changed if [[ "$BUFFER" = "$orig_buffer"* && "$orig_postdisplay" = "${BUFFER:$#orig_buffer}"* ]]; then POSTDISPLAY="${orig_postdisplay:$(($#BUFFER - $#orig_buffer))}" return $retval fi # Bail out if suggestions are disabled if (( ${+_ZSH_AUTOSUGGEST_DISABLED} )); 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() { local -i offset=${1:-0} local mode="${2:-auto}" local use_async=0 # Reset offset if the buffer changed since the last suggestion lookup if [[ "$BUFFER" != "${_ZSH_AUTOSUGGEST_LAST_PREFIX-}" ]]; then offset=0 fi if (( ${+ZSH_AUTOSUGGEST_USE_ASYNC} )); then use_async=1 fi if [[ "$mode" == "sync" ]]; then use_async=0 # Cancel any pending async request so results don't race the sync fetch _zsh_autosuggest_async_cancel fi _ZSH_AUTOSUGGEST_PENDING_OFFSET=$offset _ZSH_AUTOSUGGEST_PENDING_BUFFER="$BUFFER" if (( use_async )); then _zsh_autosuggest_async_request "$BUFFER" $offset else local suggestion _zsh_autosuggest_fetch_suggestion "$BUFFER" $offset if [[ -n "$suggestion" ]]; then _ZSH_AUTOSUGGEST_SUGGESTION_INDEX=$offset _ZSH_AUTOSUGGEST_LAST_PREFIX="$BUFFER" else if (( offset > 0 )); then _ZSH_AUTOSUGGEST_SUGGESTION_INDEX=0 fi # Ensure state tracks the current buffer even when no suggestion exists _ZSH_AUTOSUGGEST_LAST_PREFIX="$BUFFER" fi _zsh_autosuggest_suggest "$suggestion" # Clear pending markers for synchronous flow unset _ZSH_AUTOSUGGEST_PENDING_BUFFER _ZSH_AUTOSUGGEST_PENDING_OFFSET=0 fi } # Offer a suggestion _zsh_autosuggest_suggest() { emulate -L zsh local suggestion="$1" if [[ -n "$suggestion" ]] && (( $#BUFFER )); then POSTDISPLAY="${suggestion#$BUFFER}" else POSTDISPLAY= fi } # Accept the entire suggestion _zsh_autosuggest_accept() { local -i retval 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 # If we're not in a valid state to accept a suggestion, just run the # original widget and bail out if (( $CURSOR != $max_cursor_pos || !$#POSTDISPLAY )); then _zsh_autosuggest_invoke_original_widget $@ return fi # Only accept if the cursor is at the end of the buffer # Add the suggestion to the buffer BUFFER="$BUFFER$POSTDISPLAY" # Remove the suggestion POSTDISPLAY= # Run the original widget before manually moving the cursor so that the # cursor movement doesn't make the widget do something unexpected _zsh_autosuggest_invoke_original_widget $@ retval=$? # Move the cursor to the end of the buffer if [[ "$KEYMAP" = "vicmd" ]]; then CURSOR=$(($#BUFFER - 1)) else CURSOR=$#BUFFER fi return $retval } # Accept the entire suggestion and execute it _zsh_autosuggest_execute() { # Add the suggestion to the buffer BUFFER="$BUFFER$POSTDISPLAY" # Remove the suggestion 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 } () { typeset -ga _ZSH_AUTOSUGGEST_BUILTIN_ACTIONS _ZSH_AUTOSUGGEST_BUILTIN_ACTIONS=( clear fetch suggest next previous accept execute enable disable toggle ) local action for action in $_ZSH_AUTOSUGGEST_BUILTIN_ACTIONS modify partial_accept; 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 for action in $_ZSH_AUTOSUGGEST_BUILTIN_ACTIONS; do zle -N autosuggest-$action _zsh_autosuggest_widget_$action done } #--------------------------------------------------------------------# # 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() { # 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) # 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]} 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' } 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() { # 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 # Try to avoid any suggestions that wouldn't match the prefix zstyle ':completion:*' matcher-list '' zstyle ':completion:*' path-completion false zstyle ':completion:*' max-errors 0 not-numeric 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] function _complete() { unset 'compstate[vared]' _original_complete "$@" } # Open zle with buffer set so we can capture completions for it vared 1 } _zsh_autosuggest_strategy_completion() { # Reset options to defaults and enable LOCAL_OPTIONS emulate -L zsh # Enable extended glob for completion ignore pattern setopt EXTENDED_GLOB typeset -g suggestion local -i offset=${2:-0} local line # 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 # Exit if our search string matches the ignore pattern [[ -n "$ZSH_AUTOSUGGEST_COMPLETION_IGNORE" ]] && [[ "$1" == $~ZSH_AUTOSUGGEST_COMPLETION_IGNORE ]] && 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' # 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="${${(@0)line}[2]}" } always { # Destroy the pty zpty -d $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME } if [[ -n "$suggestion" ]]; then REPLY=1 if (( offset > 0 )); then unset suggestion fi else REPLY=0 fi } #--------------------------------------------------------------------# # History Suggestion Strategy # #--------------------------------------------------------------------# # Suggests the most recent history item that matches the given # prefix. # _zsh_autosuggest_strategy_history() { # Reset options to defaults and enable LOCAL_OPTIONS emulate -L zsh # Enable globbing flags so that we can use (#m) and (x~y) glob operator setopt EXTENDED_GLOB local raw_prefix="$1" local -i offset=${2:-0} # Escape backslashes and all of the glob operators so we can use # this string as a pattern to search the history list. # - (#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="${raw_prefix//(#m)[\\*?\[\]<>()|^~#]/\\$MATCH}" # Build the matcher, excluding entries that match the ignore pattern local pattern="$prefix*" if [[ -n $ZSH_AUTOSUGGEST_HISTORY_IGNORE ]]; then pattern="($pattern)~($ZSH_AUTOSUGGEST_HISTORY_IGNORE)" fi local fc_output fc_output=$(builtin fc -ln 2>/dev/null) local -a matches matches=() local line for line in ${(f)fc_output}; do if [[ "$line" == ${~pattern} ]]; then matches=("$line" "${matches[@]}") fi done # no-op: keep vars local to this function to avoid global pollution REPLY=$#matches if (( offset < $#matches )); then typeset -g suggestion="${matches[offset+1]}" else unset suggestion fi } #--------------------------------------------------------------------# # 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) and (x~y) glob operator setopt EXTENDED_GLOB local raw_prefix="$1" local -i offset=${2:-0} # TODO: Use (b) flag when we can drop support for zsh older than v5.0.8 local prefix="${raw_prefix//(#m)[\\*?[\]<>()|^~#]/\\$MATCH}" # Get the history items that match the prefix, excluding those that match # the ignore pattern local pattern="$prefix*" if [[ -n $ZSH_AUTOSUGGEST_HISTORY_IGNORE ]]; then pattern="($pattern)~($ZSH_AUTOSUGGEST_HISTORY_IGNORE)" fi # Get all history event numbers that correspond to history entries that match the pattern local history_match_keys history_match_keys=(${(kOn)history[(R)$pattern]}) # 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 local -a ordered_keys matches if [[ -n "$histkey" ]]; then ordered_keys=("$histkey") fi for key in "${(@)history_match_keys[1,200]}"; do [[ -n "$key" ]] || continue [[ "$key" = "$histkey" ]] && continue ordered_keys+=("$key") done matches=() for key in $ordered_keys; do matches+=("${history[$key]}") done REPLY=$#matches if (( offset < $#matches )); then typeset -g suggestion="${matches[offset+1]}" else unset suggestion fi } #--------------------------------------------------------------------# # Fetch Suggestion # #--------------------------------------------------------------------# # Loops through all specified strategies and returns a suggestion # from the first strategy to provide one. # _zsh_autosuggest_fetch_suggestion() { typeset -g suggestion local prefix="$1" local -i remaining_offset=${2:-0} local -a strategies local strategy local reply_value local -i strategy_count # Ensure we are working with an array strategies=(${=ZSH_AUTOSUGGEST_STRATEGY}) # Reset global suggestion result unset suggestion for strategy in $strategies; do reply_value= REPLY= # Try to get a suggestion from this strategy _zsh_autosuggest_strategy_$strategy "$prefix" $remaining_offset # Ensure the suggestion matches the prefix if [[ "$suggestion" != "$prefix"* ]]; then unset suggestion fi # Break once we've found a valid suggestion if [[ -n "$suggestion" ]]; then break fi # Determine how many suggestions this strategy can offer so we can # decrement the remaining offset before trying the next strategy. reply_value="$REPLY" if [[ -n "$reply_value" && "$reply_value" = <-> ]]; then strategy_count=$reply_value elif [[ -n "$reply_value" ]]; then # Treat non-numeric replies as zero to avoid arithmetic errors strategy_count=0 else # Preserve existing behaviour when the strategy doesn't report a count. strategy_count=0 fi if (( remaining_offset > 0 && strategy_count > 0 )); then if (( remaining_offset >= strategy_count )); then remaining_offset=$((remaining_offset - strategy_count)) else remaining_offset=0 fi fi done } #--------------------------------------------------------------------# # Async # #--------------------------------------------------------------------# _zsh_autosuggest_async_cancel() { if [[ -n "$_ZSH_AUTOSUGGEST_ASYNC_FD" ]] && { true <&$_ZSH_AUTOSUGGEST_ASYNC_FD } 2>/dev/null; then # Close the file descriptor and remove the handler builtin exec {_ZSH_AUTOSUGGEST_ASYNC_FD}<&- zle -F $_ZSH_AUTOSUGGEST_ASYNC_FD fi if [[ -n "$_ZSH_AUTOSUGGEST_CHILD_PID" ]]; then # 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 _ZSH_AUTOSUGGEST_ASYNC_FD= _ZSH_AUTOSUGGEST_CHILD_PID= } _zsh_autosuggest_async_request() { zmodload zsh/system 2>/dev/null # For `$sysparams` typeset -g _ZSH_AUTOSUGGEST_ASYNC_FD _ZSH_AUTOSUGGEST_CHILD_PID local buffer="$1" local -i offset=${2:-0} _zsh_autosuggest_async_cancel typeset -g _ZSH_AUTOSUGGEST_PENDING_BUFFER _ZSH_AUTOSUGGEST_PENDING_OFFSET _ZSH_AUTOSUGGEST_PENDING_BUFFER="$buffer" _ZSH_AUTOSUGGEST_PENDING_OFFSET=$offset # Fork a process to fetch a suggestion and open a pipe to read from it builtin exec {_ZSH_AUTOSUGGEST_ASYNC_FD}< <( # Tell parent process our pid echo $sysparams[pid] # Fetch and print the suggestion local suggestion _zsh_autosuggest_fetch_suggestion "$buffer" $offset echo -nE "$suggestion" ) # There's a weird bug here where ^C stops working unless we force a fork # See https://github.com/zsh-users/zsh-autosuggestions/issues/364 autoload -Uz is-at-least is-at-least 5.8 || command true # Read the pid from the child process read _ZSH_AUTOSUGGEST_CHILD_PID <&$_ZSH_AUTOSUGGEST_ASYNC_FD # 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 pipe # First arg will be fd ready for reading # Second arg will be passed in case of error _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 IFS='' read -rd '' -u $1 suggestion if [[ -n "$suggestion" && "$suggestion" = "$BUFFER"* ]]; then typeset -g _ZSH_AUTOSUGGEST_SUGGESTION_INDEX _ZSH_AUTOSUGGEST_LAST_PREFIX if [[ -n "${_ZSH_AUTOSUGGEST_PENDING_BUFFER-}" && "$BUFFER" = "$_ZSH_AUTOSUGGEST_PENDING_BUFFER" ]]; then _ZSH_AUTOSUGGEST_SUGGESTION_INDEX=${_ZSH_AUTOSUGGEST_PENDING_OFFSET:-0} _ZSH_AUTOSUGGEST_LAST_PREFIX="$BUFFER" else # Fall back to updating state using the current buffer if it still # matches the suggestion. _ZSH_AUTOSUGGEST_SUGGESTION_INDEX=${_ZSH_AUTOSUGGEST_PENDING_OFFSET:-0} _ZSH_AUTOSUGGEST_LAST_PREFIX="$BUFFER" fi elif [[ -z "$suggestion" ]]; then typeset -g _ZSH_AUTOSUGGEST_SUGGESTION_INDEX _ZSH_AUTOSUGGEST_LAST_PREFIX _ZSH_AUTOSUGGEST_SUGGESTION_INDEX=0 _ZSH_AUTOSUGGEST_LAST_PREFIX="$BUFFER" fi zle autosuggest-suggest -- "$suggestion" # Close the fd builtin exec {1}<&- fi # Always remove the handler zle -F "$1" _ZSH_AUTOSUGGEST_ASYNC_FD= _ZSH_AUTOSUGGEST_CHILD_PID= typeset -g _ZSH_AUTOSUGGEST_PENDING_BUFFER _ZSH_AUTOSUGGEST_PENDING_OFFSET unset _ZSH_AUTOSUGGEST_PENDING_BUFFER _ZSH_AUTOSUGGEST_PENDING_OFFSET=0 } #--------------------------------------------------------------------# # Start # #--------------------------------------------------------------------# # Start the autosuggestion widgets _zsh_autosuggest_start() { # By default we 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. However this has # a decent performance hit, so users can set ZSH_AUTOSUGGEST_MANUAL_REBIND # to disable the automatic re-binding. if (( ${+ZSH_AUTOSUGGEST_MANUAL_REBIND} )); then add-zsh-hook -d precmd _zsh_autosuggest_start fi _zsh_autosuggest_bind_widgets } # Mark for auto-loading the functions that we use autoload -Uz add-zsh-hook is-at-least # Automatically enable asynchronous mode in newer versions of zsh. Disable for # older versions because there is a bug when using async mode where ^C does not # work immediately after fetching a suggestion. # See https://github.com/zsh-users/zsh-autosuggestions/issues/364 if is-at-least 5.0.8; then typeset -g ZSH_AUTOSUGGEST_USE_ASYNC= fi # Start the autosuggestion widgets on the next precmd add-zsh-hook precmd _zsh_autosuggest_start