From 6001e1f0f52885527695e11f6b2c288969d17e4a Mon Sep 17 00:00:00 2001 From: Geza Lore Date: Sat, 5 Mar 2016 13:46:17 +0000 Subject: [PATCH 1/6] Refactor to reverse internal enumeration order. This is a pure refactoring patch and does note change any functionality. We remove an unnecessary reversal of the history match array by swapping the enumeration order of match indices. Older entries now have higher internal indices than younger entries, and hence we start from 1. Also removed one unreachable elif clause each from search-up and search-down. --- zsh-history-substring-search.zsh | 192 +++++++++++++------------------ 1 file changed, 78 insertions(+), 114 deletions(-) diff --git a/zsh-history-substring-search.zsh b/zsh-history-substring-search.zsh index ad316ac..34c171d 100644 --- a/zsh-history-substring-search.zsh +++ b/zsh-history-substring-search.zsh @@ -201,40 +201,37 @@ _history-substring-search-begin() { # Find all occurrences of the search query in the history file. # # (k) returns the "keys" (history index numbers) instead of the values - # (Oa) reverses the order, because (R) returns results reversed. + # (R) returns values in reverse older, so the index of the youngest + # matching history entry is at the head of the list. # - _history_substring_search_matches=(${(kOa)history[(R)(#$HISTORY_SUBSTRING_SEARCH_GLOBBING_FLAGS)*${_history_substring_search_query_escaped}*]}) + _history_substring_search_matches=(${(k)history[(R)(#$HISTORY_SUBSTRING_SEARCH_GLOBBING_FLAGS)*${_history_substring_search_query_escaped}*]}) # # Define the range of values that $_history_substring_search_match_index - # can take: [0, $_history_substring_search_matches_count_plus]. + # can take: [0, $_history_substring_search_matches_count + 1]. # _history_substring_search_matches_count=$#_history_substring_search_matches - _history_substring_search_matches_count_plus=$(( _history_substring_search_matches_count + 1 )) - _history_substring_search_matches_count_sans=$(( _history_substring_search_matches_count - 1 )) # # If $_history_substring_search_match_index is equal to - # $_history_substring_search_matches_count_plus, this indicates that we - # are beyond the beginning of $_history_substring_search_matches. + # $_history_substring_search_matches_count + 1, this indicates that we + # are beyond the end of $_history_substring_search_matches. # # If $_history_substring_search_match_index is equal to 0, this indicates - # that we are beyond the end of $_history_substring_search_matches. + # that we are beyond the beginning of $_history_substring_search_matches. # # If we have initially pressed "up" we have to initialize - # $_history_substring_search_match_index to - # $_history_substring_search_matches_count_plus so that it will be - # decreased to $_history_substring_search_matches_count. + # $_history_substring_search_match_index to 0 so that it will be + # incremented to 1. # # If we have initially pressed "down" we have to initialize - # $_history_substring_search_match_index to - # $_history_substring_search_matches_count so that it will be increased to - # $_history_substring_search_matches_count_plus. + # $_history_substring_search_match_index to 1 so that it will be + # decremented to 0. # if [[ $WIDGET == history-substring-search-down ]]; then - _history_substring_search_match_index=$_history_substring_search_matches_count + _history_substring_search_match_index=1 else - _history_substring_search_match_index=$_history_substring_search_matches_count_plus + _history_substring_search_match_index=0 fi fi } @@ -244,7 +241,7 @@ _history-substring-search-end() { _history_substring_search_result=$BUFFER - # the search was succesful so display the result properly by clearing away + # the search was successful so display the result properly by clearing away # existing highlights and moving the cursor to the end of the result buffer if [[ $_history_substring_search_refresh_display -eq 1 ]]; then region_highlight=() @@ -378,12 +375,30 @@ _history-substring-search-down-history() { return 1 } +_history-substring-search-found() { + # + # A match is available. The index of the match is held in + # $_history_substring_search_match_index + # + # 1. Make $BUFFER equal to the matching history entry. + # + # 2. Use $HISTORY_SUBSTRING_SEARCH_HIGHLIGHT_FOUND + # to highlight the current buffer. + # + BUFFER=$history[$_history_substring_search_matches[$_history_substring_search_match_index]] + _history_substring_search_query_highlight=$HISTORY_SUBSTRING_SEARCH_HIGHLIGHT_FOUND +} + _history-substring-search-not-found() { # - # Nothing matched the search query, so put it back into the $BUFFER while - # highlighting it accordingly so the user can revise it and search again. + # No more matches are available. + # + # 1. Make $BUFFER equal to $_history_substring_search_query so the user can + # revise it and search again. + # + # 2. Use $HISTORY_SUBSTRING_SEARCH_HIGHLIGHT_NOT_FOUND + # to highlight the current buffer. # - _history_substring_search_old_buffer=$BUFFER BUFFER=$_history_substring_search_query _history_substring_search_query_highlight=$HISTORY_SUBSTRING_SEARCH_HIGHLIGHT_NOT_FOUND } @@ -399,73 +414,48 @@ _history-substring-search-up-search() { # # $_history_substring_search_matches is the current list of matches # $_history_substring_search_matches_count is the current number of matches - # $_history_substring_search_matches_count_plus is the current number of matches + 1 - # $_history_substring_search_matches_count_sans is the current number of matches - 1 # $_history_substring_search_match_index is the index of the current match # # The range of values that $_history_substring_search_match_index can take - # is: [0, $_history_substring_search_matches_count_plus]. A value of 0 - # indicates that we are beyond the end of + # is: [0, $_history_substring_search_matches_count + 1]. A value of 0 + # indicates that we are beyond the beginning of # $_history_substring_search_matches. A value of - # $_history_substring_search_matches_count_plus indicates that we are beyond - # the beginning of $_history_substring_search_matches. + # $_history_substring_search_matches_count + 1 indicates that we are beyond + # the end of $_history_substring_search_matches. # # In _history-substring-search-up-search() the initial value of - # $_history_substring_search_match_index is - # $_history_substring_search_matches_count_plus. This value is set in - # _history-substring-search-begin(). _history-substring-search-up-search() - # will initially decrease it to $_history_substring_search_matches_count. + # $_history_substring_search_match_index is 0. This value is set in + # _history-substring-search-begin(). _history-substring-search-up-search() + # will initially increment it to 1. # - if [[ $_history_substring_search_match_index -ge 2 ]]; then + if [[ $_history_substring_search_match_index -lt $_history_substring_search_matches_count ]]; then # - # Highlight the next match: + # Pick the next (older) match: # - # 1. Decrease the value of $_history_substring_search_match_index. + # 1. Increment the value of $_history_substring_search_match_index. # - # 2. Use $HISTORY_SUBSTRING_SEARCH_HIGHLIGHT_FOUND - # to highlight the current buffer. + # 2. Update display (search found) # - (( _history_substring_search_match_index-- )) - BUFFER=$history[$_history_substring_search_matches[$_history_substring_search_match_index]] - _history_substring_search_query_highlight=$HISTORY_SUBSTRING_SEARCH_HIGHLIGHT_FOUND + (( _history_substring_search_match_index++ )) + _history-substring-search-found - elif [[ $_history_substring_search_match_index -eq 1 ]]; then + elif [[ $_history_substring_search_match_index -eq $_history_substring_search_matches_count ]]; then # # We will move beyond the end of $_history_substring_search_matches: # - # 1. Decrease the value of $_history_substring_search_match_index. + # 1. Increment the value of $_history_substring_search_match_index. # - # 2. Save the current buffer in $_history_substring_search_old_buffer, - # so that it can be retrieved by - # _history-substring-search-down-search() later. + # 2. Update display (search not found) # - # 3. Make $BUFFER equal to $_history_substring_search_query. - # - # 4. Use $HISTORY_SUBSTRING_SEARCH_HIGHLIGHT_NOT_FOUND - # to highlight the current buffer. - # - (( _history_substring_search_match_index-- )) + (( _history_substring_search_match_index++ )) _history-substring-search-not-found - - elif [[ $_history_substring_search_match_index -eq $_history_substring_search_matches_count_plus ]]; then - # - # We were beyond the beginning of $_history_substring_search_matches but - # UP makes us move back to $_history_substring_search_matches: - # - # 1. Decrease the value of $_history_substring_search_match_index. - # - # 2. Restore $BUFFER from $_history_substring_search_old_buffer. - # - # 3. Use $HISTORY_SUBSTRING_SEARCH_HIGHLIGHT_FOUND - # to highlight the current buffer. - # - (( _history_substring_search_match_index-- )) - BUFFER=$_history_substring_search_old_buffer - _history_substring_search_query_highlight=$HISTORY_SUBSTRING_SEARCH_HIGHLIGHT_FOUND + return else # - # We are at the beginning of history and there are no further matches. + # We are beyond the end of $_history_substring_search_matches. + # + # 1. Update display (search not found) # _history-substring-search-not-found return @@ -488,81 +478,55 @@ _history-substring-search-down-search() { _history_substring_search_refresh_display=1 # - # Highlight matches during history-substring-up-search: + # Highlight matches during history-substring-down-search: # # The following constants have been initialized in # _history-substring-search-up/down-search(): # # $_history_substring_search_matches is the current list of matches # $_history_substring_search_matches_count is the current number of matches - # $_history_substring_search_matches_count_plus is the current number of matches + 1 - # $_history_substring_search_matches_count_sans is the current number of matches - 1 # $_history_substring_search_match_index is the index of the current match # # The range of values that $_history_substring_search_match_index can take - # is: [0, $_history_substring_search_matches_count_plus]. A value of 0 - # indicates that we are beyond the end of + # is: [0, $_history_substring_search_matches_count + 1]. A value of 0 + # indicates that we are beyond the beginning of # $_history_substring_search_matches. A value of - # $_history_substring_search_matches_count_plus indicates that we are beyond - # the beginning of $_history_substring_search_matches. + # $_history_substring_search_matches_count + 1 indicates that we are beyond + # the end of $_history_substring_search_matches. # # In _history-substring-search-down-search() the initial value of - # $_history_substring_search_match_index is - # $_history_substring_search_matches_count. This value is set in - # _history-substring-search-begin(). - # _history-substring-search-down-search() will initially increase it to - # $_history_substring_search_matches_count_plus. + # $_history_substring_search_match_index is 1. This value is set in + # _history-substring-search-begin(). _history-substring-search-down-search() + # will initially decrement it to 0. # - if [[ $_history_substring_search_match_index -le $_history_substring_search_matches_count_sans ]]; then + if [[ $_history_substring_search_match_index -gt 1 ]]; then # - # Highlight the next match: + # Highlight the previous (younger) match: # - # 1. Increase $_history_substring_search_match_index by 1. + # 1. Decrement $_history_substring_search_match_index. # - # 2. Use $HISTORY_SUBSTRING_SEARCH_HIGHLIGHT_FOUND - # to highlight the current buffer. + # 2. Update display (search found) # - (( _history_substring_search_match_index++ )) - BUFFER=$history[$_history_substring_search_matches[$_history_substring_search_match_index]] - _history_substring_search_query_highlight=$HISTORY_SUBSTRING_SEARCH_HIGHLIGHT_FOUND + (( _history_substring_search_match_index-- )) + _history-substring-search-found - elif [[ $_history_substring_search_match_index -eq $_history_substring_search_matches_count ]]; then + elif [[ $_history_substring_search_match_index -eq 1 ]]; then # # We will move beyond the beginning of $_history_substring_search_matches: # - # 1. Increase $_history_substring_search_match_index by 1. + # 1. Decrement $_history_substring_search_match_index. # - # 2. Save the current buffer in $_history_substring_search_old_buffer, so - # that it can be retrieved by _history-substring-search-up-search() - # later. + # 2. Update display (search not found) # - # 3. Make $BUFFER equal to $_history_substring_search_query. - # - # 4. Use $HISTORY_SUBSTRING_SEARCH_HIGHLIGHT_NOT_FOUND - # to highlight the current buffer. - # - (( _history_substring_search_match_index++ )) + (( _history_substring_search_match_index-- )) _history-substring-search-not-found - - elif [[ $_history_substring_search_match_index -eq 0 ]]; then - # - # We were beyond the end of $_history_substring_search_matches but DOWN - # makes us move back to the $_history_substring_search_matches: - # - # 1. Increase $_history_substring_search_match_index by 1. - # - # 2. Restore $BUFFER from $_history_substring_search_old_buffer. - # - # 3. Use $HISTORY_SUBSTRING_SEARCH_HIGHLIGHT_FOUND - # to highlight the current buffer. - # - (( _history_substring_search_match_index++ )) - BUFFER=$_history_substring_search_old_buffer - _history_substring_search_query_highlight=$HISTORY_SUBSTRING_SEARCH_HIGHLIGHT_FOUND + return else # - # We are at the end of history and there are no further matches. + # We are beyond the beginning of $_history_substring_search_matches. + # + # 1. Update display (search not found) # _history-substring-search-not-found return From 7444ca57836bf4e212bd872916907bf961f3228c Mon Sep 17 00:00:00 2001 From: Geza Lore Date: Sat, 5 Mar 2016 15:21:24 +0000 Subject: [PATCH 2/6] Do not search history when query is empty. We used to do a history search when the query string was empty, which resulted in all history indices being returned, but they are never actually used as _history-substring-search-up-history and _history-substring-search-down-history will just act like ZSH up-history and down-history if the query is empty. Removing this unnecessary lookup should speed up the empty query case, especially if you have a long history. --- zsh-history-substring-search.zsh | 82 +++++++++++++++++++------------- 1 file changed, 48 insertions(+), 34 deletions(-) diff --git a/zsh-history-substring-search.zsh b/zsh-history-substring-search.zsh index 34c171d..885eb01 100644 --- a/zsh-history-substring-search.zsh +++ b/zsh-history-substring-search.zsh @@ -180,13 +180,27 @@ _history-substring-search-begin() { _history_substring_search_query_highlight= # - # Continue using the previous $_history_substring_search_result by default, - # unless the current query was cleared or a new/different query was entered. + # If the buffer is the same as the previously displayed history substring + # search result, then just keep stepping through the match list. Otherwise + # start a new search. # - if [[ -z $BUFFER || $BUFFER != $_history_substring_search_result ]]; then + if [[ -n $BUFFER && $BUFFER == $_history_substring_search_result ]]; then + return; + fi + + if [[ -z $BUFFER ]]; then # - # For the purpose of highlighting we will also keep - # a version without doubly-escaped meta characters. + # If the buffer is empty, we will just act like up-history/down-history + # in ZSH, so we do not need to actually search the history. This should + # speed things up a little. + # + _history_substring_search_query= + _history_substring_search_matches=() + + else + # + # For the purpose of highlighting we keep a copy of the original + # query string. # _history_substring_search_query=$BUFFER @@ -195,7 +209,7 @@ _history-substring-search-begin() { # we put an extra "\\" before meta characters such as "\(" and "\)", # so that they become "\\\(" and "\\\)". # - _history_substring_search_query_escaped=${BUFFER//(#m)[\][()|\\*?#<>~^]/\\$MATCH} + local escaped_query=${BUFFER//(#m)[\][()|\\*?#<>~^]/\\$MATCH} # # Find all occurrences of the search query in the history file. @@ -204,35 +218,35 @@ _history-substring-search-begin() { # (R) returns values in reverse older, so the index of the youngest # matching history entry is at the head of the list. # - _history_substring_search_matches=(${(k)history[(R)(#$HISTORY_SUBSTRING_SEARCH_GLOBBING_FLAGS)*${_history_substring_search_query_escaped}*]}) + _history_substring_search_matches=(${(k)history[(R)(#$HISTORY_SUBSTRING_SEARCH_GLOBBING_FLAGS)*${escaped_query}*]}) + fi - # - # Define the range of values that $_history_substring_search_match_index - # can take: [0, $_history_substring_search_matches_count + 1]. - # - _history_substring_search_matches_count=$#_history_substring_search_matches + # + # Define the range of values that $_history_substring_search_match_index + # can take: [0, $_history_substring_search_matches_count + 1]. + # + _history_substring_search_matches_count=$#_history_substring_search_matches - # - # If $_history_substring_search_match_index is equal to - # $_history_substring_search_matches_count + 1, this indicates that we - # are beyond the end of $_history_substring_search_matches. - # - # If $_history_substring_search_match_index is equal to 0, this indicates - # that we are beyond the beginning of $_history_substring_search_matches. - # - # If we have initially pressed "up" we have to initialize - # $_history_substring_search_match_index to 0 so that it will be - # incremented to 1. - # - # If we have initially pressed "down" we have to initialize - # $_history_substring_search_match_index to 1 so that it will be - # decremented to 0. - # - if [[ $WIDGET == history-substring-search-down ]]; then - _history_substring_search_match_index=1 - else - _history_substring_search_match_index=0 - fi + # + # If $_history_substring_search_match_index is equal to + # $_history_substring_search_matches_count + 1, this indicates that we + # are beyond the end of $_history_substring_search_matches. + # + # If $_history_substring_search_match_index is equal to 0, this indicates + # that we are beyond the beginning of $_history_substring_search_matches. + # + # If we have initially pressed "up" we have to initialize + # $_history_substring_search_match_index to 0 so that it will be + # incremented to 1. + # + # If we have initially pressed "down" we have to initialize + # $_history_substring_search_match_index to 1 so that it will be + # decremented to 0. + # + if [[ $WIDGET == history-substring-search-down ]]; then + _history_substring_search_match_index=1 + else + _history_substring_search_match_index=0 fi } @@ -256,7 +270,7 @@ _history-substring-search-end() { # # The following expression yields a variable $MBEGIN, which # indicates the begin position + 1 of the first occurrence - # of _history_substring_search_query_escaped in $BUFFER. + # of _history_substring_search_query in $BUFFER. # : ${(S)BUFFER##(#m$HISTORY_SUBSTRING_SEARCH_GLOBBING_FLAGS)($_history_substring_search_query##)} local begin=$(( MBEGIN - 1 )) From 352943873dab0828ba781392852d38cdb2a69443 Mon Sep 17 00:00:00 2001 From: Geza Lore Date: Sat, 5 Mar 2016 14:51:26 +0000 Subject: [PATCH 3/6] Add option to ensure unique search results. If HISTORY_SUBSTRING_SEARCH_ENSURE_UNIQUE is set to a non-empty string, then all displayed search results will be unique even if the shell option HIST_IGNORE_ALL_DUPS is not set. --- zsh-history-substring-search.zsh | 45 ++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/zsh-history-substring-search.zsh b/zsh-history-substring-search.zsh index 885eb01..f4e8087 100644 --- a/zsh-history-substring-search.zsh +++ b/zsh-history-substring-search.zsh @@ -44,6 +44,7 @@ HISTORY_SUBSTRING_SEARCH_HIGHLIGHT_FOUND='bg=magenta,fg=white,bold' HISTORY_SUBSTRING_SEARCH_HIGHLIGHT_NOT_FOUND='bg=red,fg=white,bold' HISTORY_SUBSTRING_SEARCH_GLOBBING_FLAGS='i' +unset HISTORY_SUBSTRING_SEARCH_ENSURE_UNIQUE #----------------------------------------------------------------------------- # the main ZLE widgets @@ -219,6 +220,50 @@ _history-substring-search-begin() { # matching history entry is at the head of the list. # _history_substring_search_matches=(${(k)history[(R)(#$HISTORY_SUBSTRING_SEARCH_GLOBBING_FLAGS)*${escaped_query}*]}) + + # + # If HISTORY_SUBSTRING_SEARCH_ENSURE_UNIQUE is set to a non-empty value, + # then ensure that only unique matches are presented to the user. + # When HIST_IGNORE_ALL_DUPS is set, ZSH already ensures a unique history, + # so in this case we do not need to do anything. + # + if [[ ! -o HIST_IGNORE_ALL_DUPS && -n "$HISTORY_SUBSTRING_SEARCH_ENSURE_UNIQUE" ]]; then + # + # We use an associative array (filter) as a 'set' data structure to + # collect only unique matches. If an entry (key) is already in the + # set (non-empty value), then we have already seen it before and will + # ignore it. Otherwise we add the entry both to the set and the list + # of unique matches + # + local -A filter + local -a unique_matches + + # + # Iterate through all matching history indices in younger to older + # order. This ensures that younger entries are always presented before + # older entries. + # + for index in $_history_substring_search_matches; do + # + # Get that actual history entry at this index + # + local entry=${history[$index]} + + if [[ -z ${filter[$entry]} ]]; then + # + # This is a new unique entry. Add it to the filter and the list + # of unique matches. + # + filter[$entry]=1 + unique_matches+=($index) + fi + done + + # + # Use the unique entries only + # + _history_substring_search_matches=($unique_matches) + fi fi # From 8224802bda08ef2e532b2a861b7e5c3b9104bf99 Mon Sep 17 00:00:00 2001 From: Geza Lore Date: Sat, 5 Mar 2016 22:58:51 +0000 Subject: [PATCH 4/6] Use lazy enumeration to keep response time fast. With HISTORY_SUBSTRING_SEARCH_ENSURE_UNIQUE, it can take a while to compute all unique search results, which would normally keep the shell unresponsive during this computation. This commit separates the retrieval of all history matches from the computation of the matches that need to be presented to the user (unique matches only if HISTORY_SUBSTRING_SEARCH_ENSURE_UNIQUE is set). The computation of the matches needing to be displayed is then done lazily, and only when the user requests a new previously undisplayed result. This keeps the response time fast while still enabling us to filter for unique search results. --- README.md | 20 +- zsh-history-substring-search.zsh | 350 ++++++++++++++++++++----------- 2 files changed, 244 insertions(+), 126 deletions(-) diff --git a/README.md b/README.md index 042714c..b96ae01 100644 --- a/README.md +++ b/README.md @@ -102,13 +102,19 @@ default values only after having loaded this script into your ZSH session. Flags" section in the zshexpn(1) man page to learn about the kinds of values you may assign to this variable. -To always receive _unique_ search results, use `setopt HIST_IGNORE_ALL_DUPS`. -Alternatively, use `setopt HIST_FIND_NO_DUPS` which makes this plugin skip -duplicate _adjacent_ search results as you cycle through them---however, this -does not guarantee that search results are unique: if your search results were -"Dog", "Dog", "HotDog", "Dog", then cycling them gives "Dog", "HotDog", "Dog". -Notice that the "Dog" search result appeared twice as you cycled through them! -If you wish to avoid this limitation, then use `setopt HIST_IGNORE_ALL_DUPS`. +* HISTORY_SUBSTRING_SEARCH_ENSURE_UNIQUE is a global variable that defines + whether all search results returned are _unique_. If set to a non-empty + value, then only unique search results are presented. It is unset by + default. An alternative way to ensure that search results are unique is + to use `setopt HIST_IGNORE_ALL_DUPS`. If both this configuration variable + and `setopt HIST_IGNORE_ALL_DUPS` are unset, then `setopt HIST_FIND_NO_DUPS` + is still respected, and makes this plugin skip duplicate _adjacent_ search + results as you cycle through them, but this does not guarantee that search + results are unique: if your search results were "Dog", "Dog", "HotDog", + "Dog", then cycling them gives "Dog", "HotDog", "Dog". Notice that the "Dog" + search result appeared twice as you cycled through them. If you wish to + receive globally unique search results only once, then set this + configuration variable, or use `setopt HIST_IGNORE_ALL_DUPS`. ------------------------------------------------------------------------------ History diff --git a/zsh-history-substring-search.zsh b/zsh-history-substring-search.zsh index f4e8087..b571d4c 100644 --- a/zsh-history-substring-search.zsh +++ b/zsh-history-substring-search.zsh @@ -6,6 +6,7 @@ # Copyright (c) 2011 Suraj N. Kurapati # Copyright (c) 2011 Sorin Ionescu # Copyright (c) 2011 Vincent Guerci +# Copyright (c) 2016 Geza Lore # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -196,7 +197,7 @@ _history-substring-search-begin() { # speed things up a little. # _history_substring_search_query= - _history_substring_search_matches=() + _history_substring_search_raw_matches=() else # @@ -219,65 +220,36 @@ _history-substring-search-begin() { # (R) returns values in reverse older, so the index of the youngest # matching history entry is at the head of the list. # - _history_substring_search_matches=(${(k)history[(R)(#$HISTORY_SUBSTRING_SEARCH_GLOBBING_FLAGS)*${escaped_query}*]}) - - # - # If HISTORY_SUBSTRING_SEARCH_ENSURE_UNIQUE is set to a non-empty value, - # then ensure that only unique matches are presented to the user. - # When HIST_IGNORE_ALL_DUPS is set, ZSH already ensures a unique history, - # so in this case we do not need to do anything. - # - if [[ ! -o HIST_IGNORE_ALL_DUPS && -n "$HISTORY_SUBSTRING_SEARCH_ENSURE_UNIQUE" ]]; then - # - # We use an associative array (filter) as a 'set' data structure to - # collect only unique matches. If an entry (key) is already in the - # set (non-empty value), then we have already seen it before and will - # ignore it. Otherwise we add the entry both to the set and the list - # of unique matches - # - local -A filter - local -a unique_matches - - # - # Iterate through all matching history indices in younger to older - # order. This ensures that younger entries are always presented before - # older entries. - # - for index in $_history_substring_search_matches; do - # - # Get that actual history entry at this index - # - local entry=${history[$index]} - - if [[ -z ${filter[$entry]} ]]; then - # - # This is a new unique entry. Add it to the filter and the list - # of unique matches. - # - filter[$entry]=1 - unique_matches+=($index) - fi - done - - # - # Use the unique entries only - # - _history_substring_search_matches=($unique_matches) - fi + _history_substring_search_raw_matches=(${(k)history[(R)(#$HISTORY_SUBSTRING_SEARCH_GLOBBING_FLAGS)*${escaped_query}*]}) fi # - # Define the range of values that $_history_substring_search_match_index - # can take: [0, $_history_substring_search_matches_count + 1]. + # In order to stay as responsive as possible, we will process the raw + # matches lazily (when the user requests the next match) to choose items + # that need to be displayed to the user. + # _history_substring_search_raw_match_index holds the index of the last + # unprocessed entry in _history_substring_search_raw_matches. Any items + # that need to be displayed will be added to + # _history_substring_search_matches. # - _history_substring_search_matches_count=$#_history_substring_search_matches + # We use an associative array (_history_substring_search_unique_filter) as + # a 'set' data structure to ensure uniqueness of the results if desired. + # If an entry (key) is in the set (non-empty value), then we have already + # added that entry to _history_substring_search_matches. + # + _history_substring_search_raw_match_index=0 + _history_substring_search_matches=() + unset _history_substring_search_unique_filter + typeset -A -g _history_substring_search_unique_filter # # If $_history_substring_search_match_index is equal to - # $_history_substring_search_matches_count + 1, this indicates that we - # are beyond the end of $_history_substring_search_matches. + # $#_history_substring_search_matches + 1, this indicates that we + # are beyond the end of $_history_substring_search_matches and that we + # have also processed all entries in + # _history_substring_search_raw_matches. # - # If $_history_substring_search_match_index is equal to 0, this indicates + # If $#_history_substring_search_match_index is equal to 0, this indicates # that we are beyond the beginning of $_history_substring_search_matches. # # If we have initially pressed "up" we have to initialize @@ -434,6 +406,119 @@ _history-substring-search-down-history() { return 1 } +_history_substring_search_process_raw_matches() { + # + # Process more outstanding raw matches and append any matches that need to + # be displayed to the user to _history_substring_search_matches. + # Return whether there were any more results appended. + # + + # + # While we have more raw matches. Process them to see if there are any more + # matches that need to be displayed to the user. + # + while [[ $_history_substring_search_raw_match_index -lt $#_history_substring_search_raw_matches ]]; do + # + # Move on to the next raw entry and get its history index. + # + (( _history_substring_search_raw_match_index++ )) + local index=${_history_substring_search_raw_matches[$_history_substring_search_raw_match_index]} + + # + # If HISTORY_SUBSTRING_SEARCH_ENSURE_UNIQUE is set to a non-empty value, + # then ensure that only unique matches are presented to the user. + # When HIST_IGNORE_ALL_DUPS is set, ZSH already ensures a unique history, + # so in this case we do not need to do anything. + # + if [[ ! -o HIST_IGNORE_ALL_DUPS && -n $HISTORY_SUBSTRING_SEARCH_ENSURE_UNIQUE ]]; then + # + # Get the actual history entry at the new index, and check if we have + # already added it to _history_substring_search_matches. + # + local entry=${history[$index]} + + if [[ -z ${_history_substring_search_unique_filter[$entry]} ]]; then + # + # This is a new unique entry. Add it to the filter and append the + # index to _history_substring_search_matches. + # + _history_substring_search_unique_filter[$entry]=1 + _history_substring_search_matches+=($index) + + # + # Indicate that we did find a match. + # + return 0 + fi + + else + # + # Just append the new history index to the processed matches. + # + _history_substring_search_matches+=($index) + + # + # Indicate that we did find a match. + # + return 0 + fi + + done + + # + # We are beyond the end of the list of raw matches. Indicate that no + # more matches are available. + # + return 1 +} + +_history-substring-search-has-next() { + # + # Predicate function that returns whether any more older matches are + # available. + # + + if [[ $_history_substring_search_match_index -lt $#_history_substring_search_matches ]]; then + # + # We did not reach the end of the processed list, so we do have further + # matches. + # + return 0 + + else + # + # We are at the end of the processed list. Try to process further + # unprocessed matches. _history_substring_search_process_raw_matches + # returns whether any more matches were available, so just return + # that result. + # + _history_substring_search_process_raw_matches + return $? + fi +} + +_history-substring-search-has-prev() { + # + # Predicate function that returns whether any more younger matches are + # available. + # + + if [[ $_history_substring_search_match_index -gt 1 ]]; then + # + # We did not reach the beginning of the processed list, so we do have + # further matches. + # + return 0 + + else + # + # We are at the beginning of the processed list. We do not have any more + # matches. + # + return 1 + fi +} + _history-substring-search-found() { # # A match is available. The index of the match is held in @@ -466,66 +551,84 @@ _history-substring-search-up-search() { _history_substring_search_refresh_display=1 # - # Highlight matches during history-substring-up-search: + # Select history entry during history-substring-down-search: # - # The following constants have been initialized in + # The following variables have been initialized in # _history-substring-search-up/down-search(): # - # $_history_substring_search_matches is the current list of matches - # $_history_substring_search_matches_count is the current number of matches + # $_history_substring_search_matches is the current list of matches that + # need to be displayed to the user. # $_history_substring_search_match_index is the index of the current match + # that is being displayed to the user. # # The range of values that $_history_substring_search_match_index can take - # is: [0, $_history_substring_search_matches_count + 1]. A value of 0 + # is: [0, $#_history_substring_search_matches + 1]. A value of 0 # indicates that we are beyond the beginning of # $_history_substring_search_matches. A value of - # $_history_substring_search_matches_count + 1 indicates that we are beyond - # the end of $_history_substring_search_matches. + # $#_history_substring_search_matches + 1 indicates that we are beyond + # the end of $_history_substring_search_matches and that we have also + # processed all entries in _history_substring_search_raw_matches. + # + # If $_history_substring_search_match_index equals + # $#_history_substring_search_matches and + # $_history_substring_search_raw_match_index is not greater than + # $#_history_substring_search_raw_matches, then we need to further process + # $_history_substring_search_raw_matches to see if there are any more + # entries that need to be displayed to the user. # # In _history-substring-search-up-search() the initial value of # $_history_substring_search_match_index is 0. This value is set in # _history-substring-search-begin(). _history-substring-search-up-search() # will initially increment it to 1. # - if [[ $_history_substring_search_match_index -lt $_history_substring_search_matches_count ]]; then - # - # Pick the next (older) match: - # - # 1. Increment the value of $_history_substring_search_match_index. - # - # 2. Update display (search found) - # - (( _history_substring_search_match_index++ )) - _history-substring-search-found - elif [[ $_history_substring_search_match_index -eq $_history_substring_search_matches_count ]]; then + if [[ $_history_substring_search_match_index -gt $#_history_substring_search_matches ]]; then # - # We will move beyond the end of $_history_substring_search_matches: + # We are beyond the end of $_history_substring_search_matches. This + # can only happen if we have also exhausted the unprocessed matches in + # _history_substring_search_raw_matches. # - # 1. Increment the value of $_history_substring_search_match_index. - # - # 2. Update display (search not found) - # - (( _history_substring_search_match_index++ )) - _history-substring-search-not-found - return - - else - # - # We are beyond the end of $_history_substring_search_matches. - # - # 1. Update display (search not found) + # 1. Update display to indicate search not found. # _history-substring-search-not-found return fi + if _history-substring-search-has-next; then + # + # We do have older matches. + # + # 1. Move index to point to the next match. + # 2. Update display to indicate search found. + # + (( _history_substring_search_match_index++ )) + _history-substring-search-found + + else + # + # We do not have older matches. + # + # 1. Move the index beyond the end of + # _history_substring_search_matches. + # 2. Update display to indicate search not found. + # + (( _history_substring_search_match_index++ )) + _history-substring-search-not-found + fi + # # When HIST_FIND_NO_DUPS is set, meaning that only unique command lines from # history should be matched, make sure the new and old results are different. - # But when HIST_IGNORE_ALL_DUPS is set, ZSH already ensures a unique history. # - if [[ ! -o HIST_IGNORE_ALL_DUPS && -o HIST_FIND_NO_DUPS && $BUFFER == $_history_substring_search_result ]]; then + # However, if the HIST_IGNORE_ALL_DUPS shell option, or + # HISTORY_SUBSTRING_SEARCH_ENSURE_UNIQUE is set, then we already have a + # unique history, so in this case we do not need to do anything. + # + if [[ -o HIST_IGNORE_ALL_DUPS || -n $HISTORY_SUBSTRING_SEARCH_ENSURE_UNIQUE ]]; then + return + fi + + if [[ -o HIST_FIND_NO_DUPS && $BUFFER == $_history_substring_search_result ]]; then # # Repeat the current search so that a different (unique) match is found. # @@ -537,66 +640,75 @@ _history-substring-search-down-search() { _history_substring_search_refresh_display=1 # - # Highlight matches during history-substring-down-search: + # Select history entry during history-substring-down-search: # - # The following constants have been initialized in + # The following variables have been initialized in # _history-substring-search-up/down-search(): # - # $_history_substring_search_matches is the current list of matches - # $_history_substring_search_matches_count is the current number of matches + # $_history_substring_search_matches is the current list of matches that + # need to be displayed to the user. # $_history_substring_search_match_index is the index of the current match + # that is being displayed to the user. # # The range of values that $_history_substring_search_match_index can take - # is: [0, $_history_substring_search_matches_count + 1]. A value of 0 + # is: [0, $#_history_substring_search_matches + 1]. A value of 0 # indicates that we are beyond the beginning of # $_history_substring_search_matches. A value of - # $_history_substring_search_matches_count + 1 indicates that we are beyond - # the end of $_history_substring_search_matches. + # $#_history_substring_search_matches + 1 indicates that we are beyond + # the end of $_history_substring_search_matches and that we have also + # processed all entries in _history_substring_search_raw_matches. # # In _history-substring-search-down-search() the initial value of # $_history_substring_search_match_index is 1. This value is set in # _history-substring-search-begin(). _history-substring-search-down-search() # will initially decrement it to 0. # - if [[ $_history_substring_search_match_index -gt 1 ]]; then - # - # Highlight the previous (younger) match: - # - # 1. Decrement $_history_substring_search_match_index. - # - # 2. Update display (search found) - # - (( _history_substring_search_match_index-- )) - _history-substring-search-found - elif [[ $_history_substring_search_match_index -eq 1 ]]; then - # - # We will move beyond the beginning of $_history_substring_search_matches: - # - # 1. Decrement $_history_substring_search_match_index. - # - # 2. Update display (search not found) - # - (( _history_substring_search_match_index-- )) - _history-substring-search-not-found - return - - else + if [[ $_history_substring_search_match_index -lt 1 ]]; then # # We are beyond the beginning of $_history_substring_search_matches. # - # 1. Update display (search not found) + # 1. Update display to indicate search not found. # _history-substring-search-not-found return fi + if _history-substring-search-has-prev; then + # + # We do have younger matches. + # + # 1. Move index to point to the previous match. + # 2. Update display to indicate search found. + # + (( _history_substring_search_match_index-- )) + _history-substring-search-found + + else + # + # We do not have younger matches. + # + # 1. Move the index beyond the beginning of + # _history_substring_search_matches. + # 2. Update display to indicate search not found. + # + (( _history_substring_search_match_index-- )) + _history-substring-search-not-found + fi + # # When HIST_FIND_NO_DUPS is set, meaning that only unique command lines from # history should be matched, make sure the new and old results are different. - # But when HIST_IGNORE_ALL_DUPS is set, ZSH already ensures a unique history. # - if [[ ! -o HIST_IGNORE_ALL_DUPS && -o HIST_FIND_NO_DUPS && $BUFFER == $_history_substring_search_result ]]; then + # However, if the HIST_IGNORE_ALL_DUPS shell option, or + # HISTORY_SUBSTRING_SEARCH_ENSURE_UNIQUE is set, then we already have a + # unique history, so in this case we do not need to do anything. + # + if [[ -o HIST_IGNORE_ALL_DUPS || -n $HISTORY_SUBSTRING_SEARCH_ENSURE_UNIQUE ]]; then + return + fi + + if [[ -o HIST_FIND_NO_DUPS && $BUFFER == $_history_substring_search_result ]]; then # # Repeat the current search so that a different (unique) match is found. # From b546105d7e7ebbe909823f637d32b9413f5dfe4a Mon Sep 17 00:00:00 2001 From: Geza Lore Date: Sun, 22 May 2016 09:30:52 +0100 Subject: [PATCH 5/6] Initialize option to empty string instead of unsetting. --- README.md | 12 ++++++------ zsh-history-substring-search.zsh | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index b96ae01..458702f 100644 --- a/README.md +++ b/README.md @@ -104,16 +104,16 @@ default values only after having loaded this script into your ZSH session. * HISTORY_SUBSTRING_SEARCH_ENSURE_UNIQUE is a global variable that defines whether all search results returned are _unique_. If set to a non-empty - value, then only unique search results are presented. It is unset by - default. An alternative way to ensure that search results are unique is - to use `setopt HIST_IGNORE_ALL_DUPS`. If both this configuration variable - and `setopt HIST_IGNORE_ALL_DUPS` are unset, then `setopt HIST_FIND_NO_DUPS` - is still respected, and makes this plugin skip duplicate _adjacent_ search + value, then only unique search results are presented. This behaviour is off + by default. An alternative way to ensure that search results are unique is + to use `setopt HIST_IGNORE_ALL_DUPS`. If this configuration variable is off + and `setopt HIST_IGNORE_ALL_DUPS` is unset, then `setopt HIST_FIND_NO_DUPS` + is still respected and it makes this plugin skip duplicate _adjacent_ search results as you cycle through them, but this does not guarantee that search results are unique: if your search results were "Dog", "Dog", "HotDog", "Dog", then cycling them gives "Dog", "HotDog", "Dog". Notice that the "Dog" search result appeared twice as you cycled through them. If you wish to - receive globally unique search results only once, then set this + receive globally unique search results only once, then use this configuration variable, or use `setopt HIST_IGNORE_ALL_DUPS`. ------------------------------------------------------------------------------ diff --git a/zsh-history-substring-search.zsh b/zsh-history-substring-search.zsh index b571d4c..dbfddbe 100644 --- a/zsh-history-substring-search.zsh +++ b/zsh-history-substring-search.zsh @@ -45,7 +45,7 @@ HISTORY_SUBSTRING_SEARCH_HIGHLIGHT_FOUND='bg=magenta,fg=white,bold' HISTORY_SUBSTRING_SEARCH_HIGHLIGHT_NOT_FOUND='bg=red,fg=white,bold' HISTORY_SUBSTRING_SEARCH_GLOBBING_FLAGS='i' -unset HISTORY_SUBSTRING_SEARCH_ENSURE_UNIQUE +HISTORY_SUBSTRING_SEARCH_ENSURE_UNIQUE='' #----------------------------------------------------------------------------- # the main ZLE widgets From 3e64e0e9fa944b9e95de46e8a459ecf209663fa7 Mon Sep 17 00:00:00 2001 From: Geza Lore Date: Mon, 23 May 2016 21:42:08 +0100 Subject: [PATCH 6/6] Fix repeat search with HIST_FIND_NO_DUPS. --- zsh-history-substring-search.zsh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/zsh-history-substring-search.zsh b/zsh-history-substring-search.zsh index dbfddbe..80cdab0 100644 --- a/zsh-history-substring-search.zsh +++ b/zsh-history-substring-search.zsh @@ -190,6 +190,11 @@ _history-substring-search-begin() { return; fi + # + # Clear the previous result. + # + _history_substring_search_result='' + if [[ -z $BUFFER ]]; then # # If the buffer is empty, we will just act like up-history/down-history