From 09b6c1659fe9e48c4b3d96c88739eb47ff6101ac Mon Sep 17 00:00:00 2001 From: Marcelo Lima Date: Sun, 4 Nov 2018 19:38:51 +0100 Subject: [PATCH] Add history cycling widgets --- README.md | 2 + spec/widgets/next_spec.rb | 48 +++++++++++++++++ spec/widgets/previous_spec.rb | 58 ++++++++++++++++++++ src/async.zsh | 25 ++++++--- src/strategies/default.zsh | 18 +++++-- src/strategies/match_prev_cmd.zsh | 10 ++-- src/widgets.zsh | 35 ++++++++++-- zsh-autosuggestions.zsh | 88 ++++++++++++++++++++++++------- 8 files changed, 244 insertions(+), 40 deletions(-) create mode 100644 spec/widgets/next_spec.rb create mode 100644 spec/widgets/previous_spec.rb diff --git a/README.md b/README.md index 4ad07d8..0efab9f 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,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-next`: Suggests the next newer entry from history. 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..6c2344c --- /dev/null +++ b/spec/widgets/next_spec.rb @@ -0,0 +1,48 @@ +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 +end diff --git a/spec/widgets/previous_spec.rb b/spec/widgets/previous_spec.rb new file mode 100644 index 0000000..d9ab8bd --- /dev/null +++ b/spec/widgets/previous_spec.rb @@ -0,0 +1,58 @@ +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 +end diff --git a/src/async.zsh b/src/async.zsh index 9a0cfaa..0326895 100644 --- a/src/async.zsh +++ b/src/async.zsh @@ -25,15 +25,21 @@ _zsh_autosuggest_async_server() { local last_pid - while IFS='' read -r -d $'\0' query; do + while IFS='' read -r -d $'\0' input; do # Kill last bg process kill -KILL $last_pid &>/dev/null + # Break up the input into a list + # - (p) recognize the same escape sequences as the print builtin + # - (s) force field splitting at the separator given '\1' + local query=( ${(ps:\1:)input} ) + # Run suggestion search in the background ( local suggestion - _zsh_autosuggest_strategy_$ZSH_AUTOSUGGEST_STRATEGY "$query" - echo -n -E "$suggestion"$'\0' + local capped_history_index + _zsh_autosuggest_strategy_$ZSH_AUTOSUGGEST_STRATEGY "${query[1]}" "${query[2]}" + echo -n -E "$capped_history_index"$'\1'"$suggestion"$'\0' ) & last_pid=$! @@ -42,7 +48,7 @@ _zsh_autosuggest_async_server() { _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' + zpty -w -n $ZSH_AUTOSUGGEST_ASYNC_PTY_NAME "${1}"$'\1'"${2}"$'\0' } # Called when new data is ready to be read from the pty @@ -51,10 +57,15 @@ _zsh_autosuggest_async_request() { _zsh_autosuggest_async_response() { setopt LOCAL_OPTIONS EXTENDED_GLOB - local suggestion + local raw_input - zpty -rt $ZSH_AUTOSUGGEST_ASYNC_PTY_NAME suggestion '*'$'\0' 2>/dev/null - zle autosuggest-suggest -- "${suggestion%%$'\0'##}" + zpty -rt $ZSH_AUTOSUGGEST_ASYNC_PTY_NAME raw_input '*'$'\0' 2>/dev/null + + local input=( ${(ps:\1:)raw_input%%$'\0'##} ) + local capped_history_index="${input[1]}" + local suggestion="${input[2]}" + + zle autosuggest-suggest -- "${capped_history_index}" "${suggestion}" } _zsh_autosuggest_async_pty_create() { diff --git a/src/strategies/default.zsh b/src/strategies/default.zsh index 0e85fb5..e00d11c 100644 --- a/src/strategies/default.zsh +++ b/src/strategies/default.zsh @@ -2,8 +2,8 @@ #--------------------------------------------------------------------# # Default 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_default() { @@ -13,13 +13,21 @@ _zsh_autosuggest_strategy_default() { # 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 87bb62e..e087448 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 $@ } @@ -91,25 +92,45 @@ _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 zpty -t "$ZSH_AUTOSUGGEST_ASYNC_PTY_NAME" &>/dev/null; then - _zsh_autosuggest_async_request "$BUFFER" + _zsh_autosuggest_async_request ${history_index} "$BUFFER" else local suggestion - _zsh_autosuggest_strategy_$ZSH_AUTOSUGGEST_STRATEGY "$BUFFER" - _zsh_autosuggest_suggest "$suggestion" + local capped_history_index + _zsh_autosuggest_strategy_$ZSH_AUTOSUGGEST_STRATEGY ${history_index} "$BUFFER" + _zsh_autosuggest_suggest "$capped_history_index" "$suggestion" fi } # Offer a suggestion _zsh_autosuggest_suggest() { - 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 } @@ -130,6 +151,7 @@ _zsh_autosuggest_accept() { # Remove the suggestion unset POSTDISPLAY + history_index=1 # Move the cursor to the end of the buffer CURSOR=${#BUFFER} @@ -145,6 +167,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 @@ -186,7 +209,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 @@ -211,3 +234,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 1c3eab5..f00c328 100644 --- a/zsh-autosuggestions.zsh +++ b/zsh-autosuggestions.zsh @@ -312,6 +312,7 @@ _zsh_autosuggest_toggle() { _zsh_autosuggest_clear() { # Remove the suggestion unset POSTDISPLAY + history_index=1 _zsh_autosuggest_invoke_original_widget $@ } @@ -372,25 +373,45 @@ _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 zpty -t "$ZSH_AUTOSUGGEST_ASYNC_PTY_NAME" &>/dev/null; then - _zsh_autosuggest_async_request "$BUFFER" + _zsh_autosuggest_async_request ${history_index} "$BUFFER" else local suggestion - _zsh_autosuggest_strategy_$ZSH_AUTOSUGGEST_STRATEGY "$BUFFER" - _zsh_autosuggest_suggest "$suggestion" + local capped_history_index + _zsh_autosuggest_strategy_$ZSH_AUTOSUGGEST_STRATEGY ${history_index} "$BUFFER" + _zsh_autosuggest_suggest "$capped_history_index" "$suggestion" fi } # Offer a suggestion _zsh_autosuggest_suggest() { - 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 } @@ -411,6 +432,7 @@ _zsh_autosuggest_accept() { # Remove the suggestion unset POSTDISPLAY + history_index=1 # Move the cursor to the end of the buffer CURSOR=${#BUFFER} @@ -426,6 +448,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 @@ -467,7 +490,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 @@ -492,12 +515,14 @@ 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 #--------------------------------------------------------------------# # Default 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_default() { @@ -507,15 +532,23 @@ _zsh_autosuggest_strategy_default() { # 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}]}]}" } #--------------------------------------------------------------------# @@ -546,8 +579,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* @@ -555,13 +592,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 @@ -603,15 +640,21 @@ _zsh_autosuggest_async_server() { local last_pid - while IFS='' read -r -d $'\0' query; do + while IFS='' read -r -d $'\0' input; do # Kill last bg process kill -KILL $last_pid &>/dev/null + # Break up the input into a list + # - (p) recognize the same escape sequences as the print builtin + # - (s) force field splitting at the separator given '\1' + local query=( ${(ps:\1:)input} ) + # Run suggestion search in the background ( local suggestion - _zsh_autosuggest_strategy_$ZSH_AUTOSUGGEST_STRATEGY "$query" - echo -n -E "$suggestion"$'\0' + local capped_history_index + _zsh_autosuggest_strategy_$ZSH_AUTOSUGGEST_STRATEGY "${query[1]}" "${query[2]}" + echo -n -E "$capped_history_index"$'\1'"$suggestion"$'\0' ) & last_pid=$! @@ -620,7 +663,7 @@ _zsh_autosuggest_async_server() { _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' + zpty -w -n $ZSH_AUTOSUGGEST_ASYNC_PTY_NAME "${1}"$'\1'"${2}"$'\0' } # Called when new data is ready to be read from the pty @@ -629,10 +672,15 @@ _zsh_autosuggest_async_request() { _zsh_autosuggest_async_response() { setopt LOCAL_OPTIONS EXTENDED_GLOB - local suggestion + local raw_input - zpty -rt $ZSH_AUTOSUGGEST_ASYNC_PTY_NAME suggestion '*'$'\0' 2>/dev/null - zle autosuggest-suggest -- "${suggestion%%$'\0'##}" + zpty -rt $ZSH_AUTOSUGGEST_ASYNC_PTY_NAME raw_input '*'$'\0' 2>/dev/null + + local input=( ${(ps:\1:)raw_input%%$'\0'##} ) + local capped_history_index="${input[1]}" + local suggestion="${input[2]}" + + zle autosuggest-suggest -- "${capped_history_index}" "${suggestion}" } _zsh_autosuggest_async_pty_create() {