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() {