diff --git a/script/test-strategy-match-prev-cmd.zsh b/script/test-strategy-match-prev-cmd.zsh new file mode 100755 index 0000000..590555b --- /dev/null +++ b/script/test-strategy-match-prev-cmd.zsh @@ -0,0 +1,122 @@ +#!/usr/bin/env zsh + +SCRIPT_DIR=$(dirname "$0") +TEST_DIR=$SCRIPT_DIR/../test +DIST_DIR=$SCRIPT_DIR/../ + +# Use stub.sh for stubbing/mocking +source $TEST_DIR/stub-1.0.2.sh + +source $DIST_DIR/zsh-autosuggestions.zsh + +#--------------------------------------------------------------------# +# Match Previous Command Suggestion Strategy # +#--------------------------------------------------------------------# + +TMPHIST_FILE=/tmp/zsh-autosuggestions-test-tmp-hist + +HISTSIZE=0 # Clear history +HISTSIZE=100 + +cat > $TMPHIST_FILE <<-EOH + one + two + three + four + five + six + seven + eight + nine + ten + eleven +EOH +echo >> $TMPHIST_FILE + +fc -R $TMPHIST_FILE + +rm $TMPHIST_FILE + +ZSH_AUTOSUGGEST_STRATEGY=match_prev_cmd + +testNoMatchPrevIsOne() { + stub_and_echo _zsh_autosuggest_prev_command "one" + + assertEquals \ + "Did not pick correct suggestion for prefix 'garbage' after 'one'" \ + "" \ + "$(_zsh_autosuggest_suggestion garbage)" +} + +testMatchPrevIsOne() { + stub_and_echo _zsh_autosuggest_prev_command "one" + + assertEquals \ + "Did not pick correct suggestion for prefix 'o' after 'one'" \ + "one" \ + "$(_zsh_autosuggest_suggestion o)" + + assertEquals \ + "Did not pick correct suggestion for prefix 't' after 'one'" \ + "two" \ + "$(_zsh_autosuggest_suggestion t)" + + assertEquals \ + "Did not pick correct suggestion for prefix 'th' after 'one'" \ + "three" \ + "$(_zsh_autosuggest_suggestion th)" + + assertEquals \ + "Did not pick correct suggestion for prefix 'f' after 'one'" \ + "five" \ + "$(_zsh_autosuggest_suggestion f)" + + assertEquals \ + "Did not pick correct suggestion for prefix 'fo' after 'one" \ + "four" \ + "$(_zsh_autosuggest_suggestion fo)" +} + +testNoMatchPrevIsTwo() { + stub_and_echo _zsh_autosuggest_prev_command "two" + + assertEquals \ + "Did not pick correct suggestion for prefix 'garbage' after 'two'" \ + "" \ + "$(_zsh_autosuggest_suggestion garbage)" +} + +testMatchPrevIsTwo() { + stub_and_echo _zsh_autosuggest_prev_command "two" + + assertEquals \ + "Did not pick correct suggestion for prefix 'o' after 'two'" \ + "one" \ + "$(_zsh_autosuggest_suggestion o)" + + assertEquals \ + "Did not pick correct suggestion for prefix 't' after 'two'" \ + "three" \ + "$(_zsh_autosuggest_suggestion t)" + + assertEquals \ + "Did not pick correct suggestion for prefix 'tw' after 'two'" \ + "two" \ + "$(_zsh_autosuggest_suggestion tw)" + + assertEquals \ + "Did not pick correct suggestion for prefix 'f' after 'two'" \ + "five" \ + "$(_zsh_autosuggest_suggestion f)" + + assertEquals \ + "Did not pick correct suggestion for prefix 'fo' after 'two" \ + "four" \ + "$(_zsh_autosuggest_suggestion fo)" +} + +setopt shwordsplit +SHUNIT_PARENT=$0 + +source $TEST_DIR/shunit2-2.1.6/src/shunit2 + diff --git a/src/strategies/match_prev_cmd.zsh b/src/strategies/match_prev_cmd.zsh new file mode 100644 index 0000000..bbcea9c --- /dev/null +++ b/src/strategies/match_prev_cmd.zsh @@ -0,0 +1,50 @@ + +#--------------------------------------------------------------------# +# 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, if your have just executed: +# pwd +# ls foo +# ls bar +# pwd +# And then you start typing 'ls', then the suggestion will be 'ls foo', +# rather than 'ls bar', as your most recently executed command (pwd) +# was followed by 'ls foo' on it's previous invocation. +# + +_zsh_autosuggest_strategy_match_prev_cmd() { + local prefix="$(_zsh_autosuggest_escape_command_prefix "$1")" + + # Get all history event numbers that correspond to history + # entries that match pattern $prefix* + local history_match_keys + 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]}" + + # Get the previously executed command + local prev_cmd="$(_zsh_autosuggest_prev_command)" + prev_cmd="$(_zsh_autosuggest_escape_command_prefix $prev_cmd)" + + # 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 + + # Echo the matched history entry + echo -E "$history[$histkey]" +} + diff --git a/src/suggestion.zsh b/src/suggestion.zsh index b2b8f5a..ece0a89 100644 --- a/src/suggestion.zsh +++ b/src/suggestion.zsh @@ -19,3 +19,8 @@ _zsh_autosuggest_escape_command_prefix() { # Escape special chars in the string (requires EXTENDED_GLOB) echo -E "${1//(#m)[\\()\[\]|*?]/\\$MATCH}" } + +# Get the previously executed command (hookable for testing) +_zsh_autosuggest_prev_command() { + echo -E "${history[$((HISTCMD-1))]}" +} diff --git a/zsh-autosuggestions.zsh b/zsh-autosuggestions.zsh index a55c8ff..f6f1f9f 100644 --- a/zsh-autosuggestions.zsh +++ b/zsh-autosuggestions.zsh @@ -330,6 +330,11 @@ _zsh_autosuggest_escape_command_prefix() { echo -E "${1//(#m)[\\()\[\]|*?]/\\$MATCH}" } +# Get the previously executed command (hookable for testing) +_zsh_autosuggest_prev_command() { + echo -E "${history[$((HISTCMD-1))]}" +} + #--------------------------------------------------------------------# # Default Suggestion Strategy # #--------------------------------------------------------------------# @@ -347,6 +352,56 @@ _zsh_autosuggest_strategy_default() { echo -E "${history[$histkey]}" } +#--------------------------------------------------------------------# +# 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, if your have just executed: +# pwd +# ls foo +# ls bar +# pwd +# And then you start typing 'ls', then the suggestion will be 'ls foo', +# rather than 'ls bar', as your most recently executed command (pwd) +# was followed by 'ls foo' on it's previous invocation. +# + +_zsh_autosuggest_strategy_match_prev_cmd() { + local prefix="$(_zsh_autosuggest_escape_command_prefix "$1")" + + # Get all history event numbers that correspond to history + # entries that match pattern $prefix* + local history_match_keys + 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]}" + + # Get the previously executed command + local prev_cmd="$(_zsh_autosuggest_prev_command)" + prev_cmd="$(_zsh_autosuggest_escape_command_prefix $prev_cmd)" + + # 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 + + # Echo the matched history entry + echo -E "$history[$histkey]" +} + + #--------------------------------------------------------------------# # Start # #--------------------------------------------------------------------#