diff --git a/README.md b/README.md index 96bbcb4..fd885d3 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,8 @@ This plugin provides a few widgets that you can use with `bindkey`: 5. `autosuggest-disable`: Disables suggestions. 6. `autosuggest-enable`: Re-enables suggestions. 7. `autosuggest-toggle`: Toggles between enabled/disabled suggestions. +8. `autosuggest-next`: Suggests the next older entry from history. +9. `autosuggest-previous`: Suggests the next newer entry from history. For example, this would bind ctrl + space to accept the current suggestion. diff --git a/spec/options/strategy_spec.rb b/spec/options/strategy_spec.rb index 378d01e..f89fa7f 100644 --- a/spec/options/strategy_spec.rb +++ b/spec/options/strategy_spec.rb @@ -1,7 +1,7 @@ describe 'a suggestion for a given prefix' do let(:history_strategy) { '_zsh_autosuggest_strategy_history() { suggestion="history" }' } - let(:foobar_strategy) { '_zsh_autosuggest_strategy_foobar() { [[ "foobar baz" = $1* ]] && suggestion="foobar baz" }' } - let(:foobaz_strategy) { '_zsh_autosuggest_strategy_foobaz() { [[ "foobaz bar" = $1* ]] && suggestion="foobaz bar" }' } + let(:foobar_strategy) { '_zsh_autosuggest_strategy_foobar() { [[ "foobar baz" = $2* ]] && suggestion="foobar baz" }' } + let(:foobaz_strategy) { '_zsh_autosuggest_strategy_foobaz() { [[ "foobaz bar" = $2* ]] && suggestion="foobaz bar" }' } let(:options) { [ history_strategy ] } diff --git a/spec/widgets/next_spec.rb b/spec/widgets/next_spec.rb new file mode 100644 index 0000000..5e5e931 --- /dev/null +++ b/spec/widgets/next_spec.rb @@ -0,0 +1,63 @@ +describe 'the `autosuggest-next` widget' do + context 'when suggestions are disabled' do + before do + session. + run_command('bindkey ^B autosuggest-disable'). + run_command('bindkey ^K autosuggest-next'). + send_keys('C-b') + end + it 'will fetch and display a suggestion' do + with_history('echo hello', 'echo world', 'echo joe') do + session.send_string('echo h') + sleep 1 + expect(session.content).to eq('echo h') + session.send_keys('C-k') + wait_for { session.content }.to eq('echo hello') + session.send_string('e') + wait_for { session.content }.to eq('echo hello') + end + end + end + + context 'invoked on a populated history' do + before do + session. + run_command('bindkey ^K autosuggest-next') + end + it 'will cycle, fetch, and display a suggestion' do + with_history('echo hello', 'echo world', 'echo joe') do + session.send_string('echo') + sleep 1 + expect(session.content).to eq('echo joe') + session.send_keys('C-k') + wait_for { session.content }.to eq('echo world') + session.send_keys('C-k') + wait_for { session.content }.to eq('echo hello') + session.send_keys('C-k') + wait_for { session.content }.to eq('echo hello') + end + end + end + + context 'when async mode is enabled' do + let(:options) { ['ZSH_AUTOSUGGEST_USE_ASYNC=true', 'ZSH_AUTOSUGGEST_STRATEGY=history'] } + + before do + session. + run_command('bindkey ^K autosuggest-next') + end + it 'will cycle, fetch, and display a suggestion' do + with_history('echo hello', 'echo world', 'echo joe') do + session.send_string('echo') + sleep 1 + expect(session.content).to eq('echo joe') + session.send_keys('C-k') + wait_for { session.content }.to eq('echo world') + session.send_keys('C-k') + wait_for { session.content }.to eq('echo hello') + session.send_keys('C-k') + wait_for { session.content }.to eq('echo hello') + end + end + end +end diff --git a/spec/widgets/previous_spec.rb b/spec/widgets/previous_spec.rb new file mode 100644 index 0000000..199b3a5 --- /dev/null +++ b/spec/widgets/previous_spec.rb @@ -0,0 +1,77 @@ +describe 'the `autosuggest-previous` widget' do + context 'when suggestions are disabled' do + before do + session. + run_command('bindkey ^B autosuggest-disable'). + run_command('bindkey ^J autosuggest-previous'). + send_keys('C-b') + end + it 'will fetch and display a suggestion' do + with_history('echo hello', 'echo world', 'echo joe') do + session.send_string('echo h') + sleep 1 + expect(session.content).to eq('echo h') + session.send_keys('C-j') + wait_for { session.content }.to eq('echo hello') + session.send_string('e') + wait_for { session.content }.to eq('echo hello') + end + end + end + + context 'invoked on a populated history' do + before do + session. + run_command('bindkey ^K autosuggest-next'). + run_command('bindkey ^J autosuggest-previous') + end + it 'will cycle, fetch, and display a suggestion' do + with_history('echo hello', 'echo world', 'echo joe') do + session.send_string('echo') + sleep 1 + expect(session.content).to eq('echo joe') + session.send_keys('C-k') + wait_for { session.content }.to eq('echo world') + session.send_keys('C-k') + wait_for { session.content }.to eq('echo hello') + session.send_keys('C-k') + wait_for { session.content }.to eq('echo hello') + session.send_keys('C-j') + wait_for { session.content }.to eq('echo world') + session.send_keys('C-j') + wait_for { session.content }.to eq('echo joe') + session.send_keys('C-j') + wait_for { session.content }.to eq('echo joe') + end + end + end + + context 'when async mode is enabled' do + let(:options) { ['ZSH_AUTOSUGGEST_USE_ASYNC=true', 'ZSH_AUTOSUGGEST_STRATEGY=history'] } + + before do + session. + run_command('bindkey ^K autosuggest-next'). + run_command('bindkey ^J autosuggest-previous') + end + it 'will cycle, fetch, and display a suggestion' do + with_history('echo hello', 'echo world', 'echo joe') do + session.send_string('echo') + sleep 1 + expect(session.content).to eq('echo joe') + session.send_keys('C-k') + wait_for { session.content }.to eq('echo world') + session.send_keys('C-k') + wait_for { session.content }.to eq('echo hello') + session.send_keys('C-k') + wait_for { session.content }.to eq('echo hello') + session.send_keys('C-j') + wait_for { session.content }.to eq('echo world') + session.send_keys('C-j') + wait_for { session.content }.to eq('echo joe') + session.send_keys('C-j') + wait_for { session.content }.to eq('echo joe') + end + end + end +end diff --git a/src/async.zsh b/src/async.zsh index f1877d3..2ad9380 100644 --- a/src/async.zsh +++ b/src/async.zsh @@ -34,9 +34,10 @@ _zsh_autosuggest_async_request() { echo $sysparams[pid] # Fetch and print the suggestion + local capped_history_index local suggestion - _zsh_autosuggest_fetch_suggestion "$1" - echo -nE "$suggestion" + _zsh_autosuggest_fetch_suggestion "$@" + echo -nE "$capped_history_index" "$suggestion" ) # Read the pid from the child process @@ -52,7 +53,15 @@ _zsh_autosuggest_async_request() { _zsh_autosuggest_async_response() { if [[ -z "$2" || "$2" == "hup" ]]; then # Read everything from the fd and give it as a suggestion - zle autosuggest-suggest -- "$(cat <&$1)" + local raw_input=`cat <&$1` + + # Break up the output + # - (z) split into words using shell parsing to find the words + local input=(${(z)raw_input}) + local capped_history_index="${input[1]}" + local suggestion="${input[2,-1]}" + + zle autosuggest-suggest -- "$capped_history_index" "$suggestion" # Close the fd exec {1}<&- diff --git a/src/bind.zsh b/src/bind.zsh index bb41ef8..5cc234d 100644 --- a/src/bind.zsh +++ b/src/bind.zsh @@ -76,7 +76,7 @@ _zsh_autosuggest_bind_widget() { _zsh_autosuggest_bind_widgets() { emulate -L zsh - local widget + local widget local ignore_widgets ignore_widgets=( diff --git a/src/fetch.zsh b/src/fetch.zsh index 1517018..56370e0 100644 --- a/src/fetch.zsh +++ b/src/fetch.zsh @@ -7,6 +7,7 @@ # _zsh_autosuggest_fetch_suggestion() { + typeset -g capped_history_index typeset -g suggestion local -a strategies local strategy @@ -16,7 +17,7 @@ _zsh_autosuggest_fetch_suggestion() { for strategy in $strategies; do # Try to get a suggestion from this strategy - _zsh_autosuggest_strategy_$strategy "$1" + _zsh_autosuggest_strategy_$strategy "$@" # Break once we've found a suggestion [[ -n "$suggestion" ]] && break diff --git a/src/strategies/completion.zsh b/src/strategies/completion.zsh index 7517822..77ca226 100644 --- a/src/strategies/completion.zsh +++ b/src/strategies/completion.zsh @@ -75,6 +75,10 @@ _zsh_autosuggest_strategy_completion() { typeset -g suggestion local line REPLY + # Ignore index parameter, since it does not apply to this strategy + typeset -g capped_history_index=1 + shift + # Exit if we don't have completions whence compdef >/dev/null || return diff --git a/src/strategies/history.zsh b/src/strategies/history.zsh index a2755a5..d8d37bb 100644 --- a/src/strategies/history.zsh +++ b/src/strategies/history.zsh @@ -2,8 +2,8 @@ #--------------------------------------------------------------------# # History Suggestion Strategy # #--------------------------------------------------------------------# -# Suggests the most recent history item that matches the given -# prefix. +# Suggests the history item that matches the given prefix and history +# index # _zsh_autosuggest_strategy_history() { @@ -13,13 +13,20 @@ _zsh_autosuggest_strategy_history() { # Enable globbing flags so that we can use (#m) setopt EXTENDED_GLOB + # Extract the paramenters for this function + typeset -g capped_history_index="${1}" + local query="${2}" + # 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}" + local prefix="${query//(#m)[\\*?[\]<>()|^~#]/\\$MATCH}" # Get the history items that match - # - (r) subscript flag makes the pattern match on values - typeset -g suggestion="${history[(r)${prefix}*]}" + # - (R) subscript flag makes the pattern match on values + # - (k) returns the entry indices instead of values + local suggestions=(${(k)history[(R)$prefix*]}) + (( capped_history_index > $#suggestions )) && capped_history_index=${#suggestions} + typeset -g suggestion="${history[${suggestions[${capped_history_index}]}]}" } diff --git a/src/strategies/match_prev_cmd.zsh b/src/strategies/match_prev_cmd.zsh index f76d3c1..52754b8 100644 --- a/src/strategies/match_prev_cmd.zsh +++ b/src/strategies/match_prev_cmd.zsh @@ -27,8 +27,12 @@ _zsh_autosuggest_strategy_match_prev_cmd() { # Enable globbing flags so that we can use (#m) setopt EXTENDED_GLOB + # Extract the paramenters for this function + typeset -g capped_history_index="${1}" + local query="${2}" + # TODO: Use (b) flag when we can drop support for zsh older than v5.0.8 - local prefix="${1//(#m)[\\*?[\]<>()|^~#]/\\$MATCH}" + local prefix="${query//(#m)[\\*?[\]<>()|^~#]/\\$MATCH}" # Get all history event numbers that correspond to history # entries that match pattern $prefix* @@ -36,13 +40,13 @@ _zsh_autosuggest_strategy_match_prev_cmd() { 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]}" + local histkey="${history_match_keys[capped_history_index]}" # 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 + for key in "${(@)history_match_keys[capped_history_index,200]}"; do # Stop if we ran out of history [[ $key -gt 1 ]] || break diff --git a/src/widgets.zsh b/src/widgets.zsh index 3312579..27d6ee1 100644 --- a/src/widgets.zsh +++ b/src/widgets.zsh @@ -31,6 +31,7 @@ _zsh_autosuggest_toggle() { _zsh_autosuggest_clear() { # Remove the suggestion unset POSTDISPLAY + history_index=1 _zsh_autosuggest_invoke_original_widget $@ } @@ -50,6 +51,7 @@ _zsh_autosuggest_modify() { # Clear suggestion while waiting for next one unset POSTDISPLAY + history_index=1 # Original widget may modify the buffer _zsh_autosuggest_invoke_original_widget $@ @@ -93,14 +95,31 @@ _zsh_autosuggest_modify() { return $retval } +# Navigate to the next suggestion in the suggestion list +_zsh_autosuggest_next() { + history_index=$(( history_index + 1 )) + _zsh_autosuggest_fetch +} + +# Navigate to the previous suggestion in the suggestion list +_zsh_autosuggest_previous() { + (( history_index > 1 )) && history_index=$(( history_index - 1 )) + _zsh_autosuggest_fetch +} + # Fetch a new suggestion based on what's currently in the buffer _zsh_autosuggest_fetch() { + if ! (( history_index > 0 )); then + history_index=1 + fi + if [[ -n "${ZSH_AUTOSUGGEST_USE_ASYNC+x}" ]]; then - _zsh_autosuggest_async_request "$BUFFER" + _zsh_autosuggest_async_request "$history_index" "$BUFFER" else local suggestion - _zsh_autosuggest_fetch_suggestion "$BUFFER" - _zsh_autosuggest_suggest "$suggestion" + local capped_history_index + _zsh_autosuggest_fetch_suggestion "$history_index" "$BUFFER" + _zsh_autosuggest_suggest "$capped_history_index" "$suggestion" fi } @@ -108,12 +127,15 @@ _zsh_autosuggest_fetch() { _zsh_autosuggest_suggest() { emulate -L zsh - local suggestion="$1" + local capped_history_index="$1" + local suggestion="$2" if [[ -n "$suggestion" ]] && (( $#BUFFER )); then POSTDISPLAY="${suggestion#$BUFFER}" + history_index="${capped_history_index}" else unset POSTDISPLAY + history_index=1 fi } @@ -134,6 +156,7 @@ _zsh_autosuggest_accept() { # Remove the suggestion unset POSTDISPLAY + history_index=1 # Move the cursor to the end of the buffer CURSOR=${#BUFFER} @@ -149,6 +172,7 @@ _zsh_autosuggest_execute() { # Remove the suggestion unset POSTDISPLAY + history_index=1 # Call the original `accept-line` to handle syntax highlighting or # other potential custom behavior @@ -190,7 +214,7 @@ _zsh_autosuggest_partial_accept() { return $retval } -for action in clear modify fetch suggest accept partial_accept execute enable disable toggle; do +for action in clear modify fetch suggest accept partial_accept execute enable disable toggle next previous; do eval "_zsh_autosuggest_widget_$action() { local -i retval @@ -215,3 +239,5 @@ 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 +zle -N autosuggest-next _zsh_autosuggest_widget_next +zle -N autosuggest-previous _zsh_autosuggest_widget_previous diff --git a/zsh-autosuggestions.zsh b/zsh-autosuggestions.zsh index 4b39dda..a9151ad 100644 --- a/zsh-autosuggestions.zsh +++ b/zsh-autosuggestions.zsh @@ -186,7 +186,7 @@ _zsh_autosuggest_bind_widget() { _zsh_autosuggest_bind_widgets() { emulate -L zsh - local widget + local widget local ignore_widgets ignore_widgets=( @@ -287,6 +287,7 @@ _zsh_autosuggest_toggle() { _zsh_autosuggest_clear() { # Remove the suggestion unset POSTDISPLAY + history_index=1 _zsh_autosuggest_invoke_original_widget $@ } @@ -306,6 +307,7 @@ _zsh_autosuggest_modify() { # Clear suggestion while waiting for next one unset POSTDISPLAY + history_index=1 # Original widget may modify the buffer _zsh_autosuggest_invoke_original_widget $@ @@ -349,14 +351,31 @@ _zsh_autosuggest_modify() { return $retval } +# Navigate to the next suggestion in the suggestion list +_zsh_autosuggest_next() { + history_index=$(( history_index + 1 )) + _zsh_autosuggest_fetch +} + +# Navigate to the previous suggestion in the suggestion list +_zsh_autosuggest_previous() { + (( history_index > 1 )) && history_index=$(( history_index - 1 )) + _zsh_autosuggest_fetch +} + # Fetch a new suggestion based on what's currently in the buffer _zsh_autosuggest_fetch() { + if ! (( history_index > 0 )); then + history_index=1 + fi + if [[ -n "${ZSH_AUTOSUGGEST_USE_ASYNC+x}" ]]; then - _zsh_autosuggest_async_request "$BUFFER" + _zsh_autosuggest_async_request "$history_index" "$BUFFER" else local suggestion - _zsh_autosuggest_fetch_suggestion "$BUFFER" - _zsh_autosuggest_suggest "$suggestion" + local capped_history_index + _zsh_autosuggest_fetch_suggestion "$history_index" "$BUFFER" + _zsh_autosuggest_suggest "$capped_history_index" "$suggestion" fi } @@ -364,12 +383,15 @@ _zsh_autosuggest_fetch() { _zsh_autosuggest_suggest() { emulate -L zsh - local suggestion="$1" + local capped_history_index="$1" + local suggestion="$2" if [[ -n "$suggestion" ]] && (( $#BUFFER )); then POSTDISPLAY="${suggestion#$BUFFER}" + history_index="${capped_history_index}" else unset POSTDISPLAY + history_index=1 fi } @@ -390,6 +412,7 @@ _zsh_autosuggest_accept() { # Remove the suggestion unset POSTDISPLAY + history_index=1 # Move the cursor to the end of the buffer CURSOR=${#BUFFER} @@ -405,6 +428,7 @@ _zsh_autosuggest_execute() { # Remove the suggestion unset POSTDISPLAY + history_index=1 # Call the original `accept-line` to handle syntax highlighting or # other potential custom behavior @@ -446,7 +470,7 @@ _zsh_autosuggest_partial_accept() { return $retval } -for action in clear modify fetch suggest accept partial_accept execute enable disable toggle; do +for action in clear modify fetch suggest accept partial_accept execute enable disable toggle next previous; do eval "_zsh_autosuggest_widget_$action() { local -i retval @@ -471,6 +495,8 @@ 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 +zle -N autosuggest-next _zsh_autosuggest_widget_next +zle -N autosuggest-previous _zsh_autosuggest_widget_previous #--------------------------------------------------------------------# # Completion Suggestion Strategy # @@ -548,6 +574,10 @@ _zsh_autosuggest_strategy_completion() { typeset -g suggestion local line REPLY + # Ignore index parameter, since it does not apply to this strategy + typeset -g capped_history_index=1 + shift + # Exit if we don't have completions whence compdef >/dev/null || return @@ -579,8 +609,8 @@ _zsh_autosuggest_strategy_completion() { #--------------------------------------------------------------------# # History Suggestion Strategy # #--------------------------------------------------------------------# -# Suggests the most recent history item that matches the given -# prefix. +# Suggests the history item that matches the given prefix and history +# index # _zsh_autosuggest_strategy_history() { @@ -590,15 +620,22 @@ _zsh_autosuggest_strategy_history() { # Enable globbing flags so that we can use (#m) setopt EXTENDED_GLOB + # Extract the paramenters for this function + typeset -g capped_history_index="${1}" + local query="${2}" + # 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}" + local prefix="${query//(#m)[\\*?[\]<>()|^~#]/\\$MATCH}" # Get the history items that match - # - (r) subscript flag makes the pattern match on values - typeset -g suggestion="${history[(r)${prefix}*]}" + # - (R) subscript flag makes the pattern match on values + # - (k) returns the entry indices instead of values + local suggestions=(${(k)history[(R)$prefix*]}) + (( capped_history_index > $#suggestions )) && capped_history_index=${#suggestions} + typeset -g suggestion="${history[${suggestions[${capped_history_index}]}]}" } #--------------------------------------------------------------------# @@ -629,8 +666,12 @@ _zsh_autosuggest_strategy_match_prev_cmd() { # Enable globbing flags so that we can use (#m) setopt EXTENDED_GLOB + # Extract the paramenters for this function + typeset -g capped_history_index="${1}" + local query="${2}" + # TODO: Use (b) flag when we can drop support for zsh older than v5.0.8 - local prefix="${1//(#m)[\\*?[\]<>()|^~#]/\\$MATCH}" + local prefix="${query//(#m)[\\*?[\]<>()|^~#]/\\$MATCH}" # Get all history event numbers that correspond to history # entries that match pattern $prefix* @@ -638,13 +679,13 @@ _zsh_autosuggest_strategy_match_prev_cmd() { 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]}" + local histkey="${history_match_keys[capped_history_index]}" # 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 + for key in "${(@)history_match_keys[capped_history_index,200]}"; do # Stop if we ran out of history [[ $key -gt 1 ]] || break @@ -668,6 +709,7 @@ _zsh_autosuggest_strategy_match_prev_cmd() { # _zsh_autosuggest_fetch_suggestion() { + typeset -g capped_history_index typeset -g suggestion local -a strategies local strategy @@ -677,7 +719,7 @@ _zsh_autosuggest_fetch_suggestion() { for strategy in $strategies; do # Try to get a suggestion from this strategy - _zsh_autosuggest_strategy_$strategy "$1" + _zsh_autosuggest_strategy_$strategy "$@" # Break once we've found a suggestion [[ -n "$suggestion" ]] && break @@ -719,9 +761,10 @@ _zsh_autosuggest_async_request() { echo $sysparams[pid] # Fetch and print the suggestion + local capped_history_index local suggestion - _zsh_autosuggest_fetch_suggestion "$1" - echo -nE "$suggestion" + _zsh_autosuggest_fetch_suggestion "$@" + echo -nE "$capped_history_index" "$suggestion" ) # Read the pid from the child process @@ -737,7 +780,15 @@ _zsh_autosuggest_async_request() { _zsh_autosuggest_async_response() { if [[ -z "$2" || "$2" == "hup" ]]; then # Read everything from the fd and give it as a suggestion - zle autosuggest-suggest -- "$(cat <&$1)" + local raw_input=`cat <&$1` + + # Break up the output + # - (z) split into words using shell parsing to find the words + local input=(${(z)raw_input}) + local capped_history_index="${input[1]}" + local suggestion="${input[2,-1]}" + + zle autosuggest-suggest -- "$capped_history_index" "$suggestion" # Close the fd exec {1}<&-