From bf5d77a65f2a672eed9229793b9510d7c56a878c Mon Sep 17 00:00:00 2001 From: David Abutbul Date: Mon, 27 Oct 2025 22:03:48 +0200 Subject: [PATCH 1/2] Add autosuggest-next and autosuggest-previous widgets; enhance async suggestion handling --- .bundle/config | 2 + README.md | 8 +- spec/widgets/next_spec.rb | 38 ++++ spec/widgets/previous_spec.rb | 38 ++++ src/async.zsh | 77 +++++--- src/fetch.zsh | 41 +++- src/strategies/completion.zsh | 12 +- src/strategies/history.zsh | 36 +++- src/strategies/match_prev_cmd.zsh | 35 +++- src/widgets.zsh | 114 ++++++++++- zsh-autosuggestions.zsh | 315 ++++++++++++++++++++++++++---- 11 files changed, 629 insertions(+), 87 deletions(-) create mode 100644 .bundle/config create mode 100644 spec/widgets/next_spec.rb create mode 100644 spec/widgets/previous_spec.rb diff --git a/.bundle/config b/.bundle/config new file mode 100644 index 0000000..2369228 --- /dev/null +++ b/.bundle/config @@ -0,0 +1,2 @@ +--- +BUNDLE_PATH: "vendor/bundle" diff --git a/README.md b/README.md index a8c1b6c..d56c820 100644 --- a/README.md +++ b/README.md @@ -109,9 +109,11 @@ This plugin provides a few widgets that you can use with `bindkey`: 2. `autosuggest-execute`: Accepts and executes the current suggestion. 3. `autosuggest-clear`: Clears the current suggestion. 4. `autosuggest-fetch`: Fetches a suggestion (works even when suggestions are disabled). -5. `autosuggest-disable`: Disables suggestions. -6. `autosuggest-enable`: Re-enables suggestions. -7. `autosuggest-toggle`: Toggles between enabled/disabled suggestions. +5. `autosuggest-next`: Cycles to the next available suggestion for the current buffer (wraps around). +6. `autosuggest-previous`: Cycles backward through the available suggestions (wraps around). +7. `autosuggest-disable`: Disables suggestions. +8. `autosuggest-enable`: Re-enables suggestions. +9. `autosuggest-toggle`: Toggles between enabled/disabled suggestions. For example, this would bind ctrl + space to accept the current suggestion. diff --git a/spec/widgets/next_spec.rb b/spec/widgets/next_spec.rb new file mode 100644 index 0000000..7d0b048 --- /dev/null +++ b/spec/widgets/next_spec.rb @@ -0,0 +1,38 @@ +describe 'the `autosuggest-next` widget' do + let(:options) { ['unset ZSH_AUTOSUGGEST_USE_ASYNC', 'ZSH_AUTOSUGGEST_STRATEGY=history'] } + + before do + session.run_command('bindkey ^N autosuggest-next') + end + + it 'cycles through history suggestions and wraps around' do + with_history do + session.run_command('echo foo') + session.run_command('echo bar') + session.run_command('echo baz') + session.clear_screen + + session.send_string('echo ') + wait_for { session.content }.to eq('echo baz') + + session.send_keys('C-n') + wait_for { session.content }.to eq('echo bar') + + session.send_keys('C-n') + wait_for { session.content }.to eq('echo foo') + + session.send_keys('C-n') + wait_for { session.content }.to eq('echo baz') + end + end + + it 'leaves the buffer untouched when no suggestions are available' do + with_history do + session.send_string('foo') + wait_for { session.content }.to eq('foo') + + session.send_keys('C-n') + wait_for { session.content }.to eq('foo') + end + end +end diff --git a/spec/widgets/previous_spec.rb b/spec/widgets/previous_spec.rb new file mode 100644 index 0000000..7f0b59e --- /dev/null +++ b/spec/widgets/previous_spec.rb @@ -0,0 +1,38 @@ +describe 'the `autosuggest-previous` widget' do + let(:options) { ['unset ZSH_AUTOSUGGEST_USE_ASYNC', 'ZSH_AUTOSUGGEST_STRATEGY=history'] } + + before do + session.run_command('bindkey ^P autosuggest-previous') + end + + it 'cycles backwards through history suggestions and wraps around' do + with_history do + session.run_command('echo foo') + session.run_command('echo bar') + session.run_command('echo baz') + session.clear_screen + + session.send_string('echo ') + wait_for { session.content }.to eq('echo baz') + + session.send_keys('C-p') + wait_for { session.content }.to eq('echo foo') + + session.send_keys('C-p') + wait_for { session.content }.to eq('echo bar') + + session.send_keys('C-p') + wait_for { session.content }.to eq('echo baz') + end + end + + it 'leaves the buffer untouched when no suggestions are available' do + with_history do + session.send_string('foo') + wait_for { session.content }.to eq('foo') + + session.send_keys('C-p') + wait_for { session.content }.to eq('foo') + end + end +end diff --git a/src/async.zsh b/src/async.zsh index e179734..11e7713 100644 --- a/src/async.zsh +++ b/src/async.zsh @@ -3,33 +3,45 @@ # 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 - # 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 - builtin exec {_ZSH_AUTOSUGGEST_ASYNC_FD}<&- - zle -F $_ZSH_AUTOSUGGEST_ASYNC_FD + local buffer="$1" + local -i offset=${2:-0} - # We won't know the pid unless the user has zsh/system module installed - 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 - fi + _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}< <( @@ -38,7 +50,7 @@ _zsh_autosuggest_async_request() { # Fetch and print the suggestion local suggestion - _zsh_autosuggest_fetch_suggestion "$1" + _zsh_autosuggest_fetch_suggestion "$buffer" $offset echo -nE "$suggestion" ) @@ -65,6 +77,22 @@ _zsh_autosuggest_async_response() { 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 @@ -74,4 +102,9 @@ _zsh_autosuggest_async_response() { # 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 } diff --git a/src/fetch.zsh b/src/fetch.zsh index fef2715..2b1ff57 100644 --- a/src/fetch.zsh +++ b/src/fetch.zsh @@ -8,20 +8,55 @@ _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 "$1" + _zsh_autosuggest_strategy_$strategy "$prefix" $remaining_offset # Ensure the suggestion matches the prefix - [[ "$suggestion" != "$1"* ]] && unset suggestion + if [[ "$suggestion" != "$prefix"* ]]; then + unset suggestion + fi # Break once we've found a valid suggestion - [[ -n "$suggestion" ]] && break + 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 } diff --git a/src/strategies/completion.zsh b/src/strategies/completion.zsh index e2d114c..be0c355 100644 --- a/src/strategies/completion.zsh +++ b/src/strategies/completion.zsh @@ -101,7 +101,8 @@ _zsh_autosuggest_strategy_completion() { setopt EXTENDED_GLOB typeset -g suggestion - local line REPLY + local -i offset=${2:-0} + local line # Exit if we don't have completions whence compdef >/dev/null || return @@ -134,4 +135,13 @@ _zsh_autosuggest_strategy_completion() { # 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 } diff --git a/src/strategies/history.zsh b/src/strategies/history.zsh index 0672a13..8f74920 100644 --- a/src/strategies/history.zsh +++ b/src/strategies/history.zsh @@ -1,4 +1,3 @@ - #--------------------------------------------------------------------# # History Suggestion Strategy # #--------------------------------------------------------------------# @@ -13,20 +12,41 @@ _zsh_autosuggest_strategy_history() { # 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 associative array. + # 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="${1//(#m)[\\*?[\]<>()|^~#]/\\$MATCH}" + local prefix="${raw_prefix//(#m)[\\*?\[\]<>()|^~#]/\\$MATCH}" - # Get the history items that match the prefix, excluding those that match - # the ignore pattern + # 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 - # Give the first history item matching the pattern as the suggestion - # - (r) subscript flag makes the pattern match on values - typeset -g suggestion="${history[(r)$pattern]}" + 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 } diff --git a/src/strategies/match_prev_cmd.zsh b/src/strategies/match_prev_cmd.zsh index b709783..9768bef 100644 --- a/src/strategies/match_prev_cmd.zsh +++ b/src/strategies/match_prev_cmd.zsh @@ -27,8 +27,11 @@ _zsh_autosuggest_strategy_match_prev_cmd() { # 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="${1//(#m)[\\*?[\]<>()|^~#]/\\$MATCH}" + local prefix="${raw_prefix//(#m)[\\*?[\]<>()|^~#]/\\$MATCH}" # Get the history items that match the prefix, excluding those that match # the ignore pattern @@ -37,10 +40,9 @@ _zsh_autosuggest_strategy_match_prev_cmd() { pattern="($pattern)~($ZSH_AUTOSUGGEST_HISTORY_IGNORE)" fi - # Get all history event numbers that correspond to history - # entries that match the pattern + # Get all history event numbers that correspond to history entries that match the pattern local history_match_keys - history_match_keys=(${(k)history[(R)$~pattern]}) + 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]}" @@ -61,6 +63,27 @@ _zsh_autosuggest_strategy_match_prev_cmd() { fi done - # Give back the matched history entry - typeset -g suggestion="$history[$histkey]" + 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 } diff --git a/src/widgets.zsh b/src/widgets.zsh index 7562897..ec4dec3 100644 --- a/src/widgets.zsh +++ b/src/widgets.zsh @@ -3,6 +3,11 @@ # 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 @@ -18,6 +23,69 @@ _zsh_autosuggest_enable() { 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 @@ -31,6 +99,8 @@ _zsh_autosuggest_toggle() { _zsh_autosuggest_clear() { # Remove the suggestion POSTDISPLAY= + _ZSH_AUTOSUGGEST_SUGGESTION_INDEX=0 + _ZSH_AUTOSUGGEST_LAST_PREFIX="$BUFFER" _zsh_autosuggest_invoke_original_widget $@ } @@ -84,12 +154,50 @@ _zsh_autosuggest_modify() { # 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 - _zsh_autosuggest_async_request "$BUFFER" + 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" + _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 } @@ -200,6 +308,8 @@ _zsh_autosuggest_partial_accept() { clear fetch suggest + next + previous accept execute enable diff --git a/zsh-autosuggestions.zsh b/zsh-autosuggestions.zsh index e780225..b32eda1 100644 --- a/zsh-autosuggestions.zsh +++ b/zsh-autosuggestions.zsh @@ -267,6 +267,11 @@ _zsh_autosuggest_highlight_apply() { # 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 @@ -282,6 +287,69 @@ _zsh_autosuggest_enable() { 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 @@ -295,6 +363,8 @@ _zsh_autosuggest_toggle() { _zsh_autosuggest_clear() { # Remove the suggestion POSTDISPLAY= + _ZSH_AUTOSUGGEST_SUGGESTION_INDEX=0 + _ZSH_AUTOSUGGEST_LAST_PREFIX="$BUFFER" _zsh_autosuggest_invoke_original_widget $@ } @@ -348,12 +418,50 @@ _zsh_autosuggest_modify() { # 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 - _zsh_autosuggest_async_request "$BUFFER" + 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" + _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 } @@ -464,6 +572,8 @@ _zsh_autosuggest_partial_accept() { clear fetch suggest + next + previous accept execute enable @@ -596,7 +706,8 @@ _zsh_autosuggest_strategy_completion() { setopt EXTENDED_GLOB typeset -g suggestion - local line REPLY + local -i offset=${2:-0} + local line # Exit if we don't have completions whence compdef >/dev/null || return @@ -629,8 +740,16 @@ _zsh_autosuggest_strategy_completion() { # 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 # #--------------------------------------------------------------------# @@ -645,22 +764,43 @@ _zsh_autosuggest_strategy_history() { # 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 associative array. + # 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="${1//(#m)[\\*?[\]<>()|^~#]/\\$MATCH}" + local prefix="${raw_prefix//(#m)[\\*?\[\]<>()|^~#]/\\$MATCH}" - # Get the history items that match the prefix, excluding those that match - # the ignore pattern + # 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 - # Give the first history item matching the pattern as the suggestion - # - (r) subscript flag makes the pattern match on values - typeset -g suggestion="${history[(r)$pattern]}" + 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 } #--------------------------------------------------------------------# @@ -691,8 +831,11 @@ _zsh_autosuggest_strategy_match_prev_cmd() { # 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="${1//(#m)[\\*?[\]<>()|^~#]/\\$MATCH}" + local prefix="${raw_prefix//(#m)[\\*?[\]<>()|^~#]/\\$MATCH}" # Get the history items that match the prefix, excluding those that match # the ignore pattern @@ -701,10 +844,9 @@ _zsh_autosuggest_strategy_match_prev_cmd() { pattern="($pattern)~($ZSH_AUTOSUGGEST_HISTORY_IGNORE)" fi - # Get all history event numbers that correspond to history - # entries that match the pattern + # Get all history event numbers that correspond to history entries that match the pattern local history_match_keys - history_match_keys=(${(k)history[(R)$~pattern]}) + 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]}" @@ -725,8 +867,29 @@ _zsh_autosuggest_strategy_match_prev_cmd() { fi done - # Give back the matched history entry - typeset -g suggestion="$history[$histkey]" + 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 } #--------------------------------------------------------------------# @@ -738,21 +901,56 @@ _zsh_autosuggest_strategy_match_prev_cmd() { _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 "$1" + _zsh_autosuggest_strategy_$strategy "$prefix" $remaining_offset # Ensure the suggestion matches the prefix - [[ "$suggestion" != "$1"* ]] && unset suggestion + if [[ "$suggestion" != "$prefix"* ]]; then + unset suggestion + fi # Break once we've found a valid suggestion - [[ -n "$suggestion" ]] && break + 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 } @@ -760,33 +958,45 @@ _zsh_autosuggest_fetch_suggestion() { # 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 - # 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 - builtin exec {_ZSH_AUTOSUGGEST_ASYNC_FD}<&- - zle -F $_ZSH_AUTOSUGGEST_ASYNC_FD + local buffer="$1" + local -i offset=${2:-0} - # We won't know the pid unless the user has zsh/system module installed - 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 - fi + _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}< <( @@ -795,7 +1005,7 @@ _zsh_autosuggest_async_request() { # Fetch and print the suggestion local suggestion - _zsh_autosuggest_fetch_suggestion "$1" + _zsh_autosuggest_fetch_suggestion "$buffer" $offset echo -nE "$suggestion" ) @@ -822,6 +1032,22 @@ _zsh_autosuggest_async_response() { 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 @@ -831,6 +1057,11 @@ _zsh_autosuggest_async_response() { # 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 } #--------------------------------------------------------------------# From fc23a2c93fef8057dda87dbde56ce173375afbbd Mon Sep 17 00:00:00 2001 From: David Abutbul Date: Mon, 27 Oct 2025 22:04:51 +0200 Subject: [PATCH 2/2] Remove obsolete bundle configuration file --- .bundle/config | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 .bundle/config diff --git a/.bundle/config b/.bundle/config deleted file mode 100644 index 2369228..0000000 --- a/.bundle/config +++ /dev/null @@ -1,2 +0,0 @@ ---- -BUNDLE_PATH: "vendor/bundle"