#!/usr/bin/env zsh # # This is a clean-room implementation of the Fish[1] shell's history search # feature, where you can type in any part of any previously entered command # and press the UP and DOWN arrow keys to cycle through the matching commands. # #----------------------------------------------------------------------------- # Usage #----------------------------------------------------------------------------- # # 1. Load this script into your interactive ZSH session: # # % source history-substring-search.zsh # # If you want to use the zsh-syntax-highlighting[6] script along with this # script, then make sure that you load it *before* you load this script: # # % source zsh-syntax-highlighting.zsh # % source history-substring-search.zsh # # 2. Type any part of any previous command and then: # # * Press the UP arrow key to select the nearest command that (1) contains # your query and (2) is older than the current command in the command # history. # # * Press the DOWN arrow key to select the nearest command that (1) # contains your query and (2) is newer than the current command in the # command history. # # * Press ^U (the Control and U keys simultaneously) to abort the search. # # 3. If a matching command spans more than one line of text, press the LEFT # arrow key to move the cursor away from the end of the command, and then: # # * Press the UP arrow key to move the cursor to the line above. When the # cursor reaches the first line of the command, pressing the UP arrow # key again will cause this script to perform another search. # # * Press the DOWN arrow key to move the cursor to the line below. When # the cursor reaches the last line of the command, pressing the DOWN # arrow key again will cause this script to perform another search. # #----------------------------------------------------------------------------- # Configuration #----------------------------------------------------------------------------- # # This script defines the following global variables. You may override their # default values only after having loaded this script into your ZSH session. # # * HISTORY_SUBSTRING_SEARCH_HIGHLIGHT_FOUND is a global variable that defines # how the query should be highlighted inside a matching command. Its default # value causes this script to highlight using bold, white text on a magenta # background. See the "Character Highlighting" section in the zshzle(1) man # page to learn about the kinds of values you may assign to this variable. # # * HISTORY_SUBSTRING_SEARCH_HIGHLIGHT_NOT_FOUND is a global variable that # defines how the query should be highlighted when no commands in the # history match it. Its default value causes this script to highlight using # bold, white text on a red background. See the "Character Highlighting" # section in the zshzle(1) man page to learn about the kinds of values you # may assign to this variable. # # * HISTORY_SUBSTRING_SEARCH_GLOBBING_FLAGS is a global variable that defines # how the command history will be searched for your query. Its default value # causes this script to perform a case-insensitive search. See the "Globbing # Flags" section in the zshexpn(1) man page to learn about the kinds of # values you may assign to this variable. # #----------------------------------------------------------------------------- # History #----------------------------------------------------------------------------- # # This script was originally written by Peter Stephenson[2], who published it # to the ZSH users mailing list (thereby making it public domain) in September # 2009. It was later revised by Guido van Steen and released under the BSD # license (see below) as part of the fizsh[3] project in January 2011. # # It was later extracted from fizsh[3] release 1.0.1, refactored heavily, and # repackaged as both an oh-my-zsh plugin[4] and as an independently loadable # ZSH script[5] by Suraj N. Kurapati in 2011. # # It was further developed[4] by Guido van Steen, Suraj N. Kurapati, Sorin # Ionescu, and Vincent Guerci in 2011. # # [1]: http://fishshell.com # [2]: http://www.zsh.org/mla/users/2009/msg00818.html # [3]: http://sourceforge.net/projects/fizsh/ # [4]: https://github.com/robbyrussell/oh-my-zsh/pull/215 # [5]: https://github.com/sunaku/zsh-history-substring-search # [6]: https://github.com/nicoulaj/zsh-syntax-highlighting # ############################################################################## # # Copyright (c) 2009 Peter Stephenson # Copyright (c) 2011 Guido van Steen # Copyright (c) 2011 Suraj N. Kurapati # Copyright (c) 2011 Sorin Ionescu # Copyright (c) 2011 Vincent Guerci # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following # disclaimer in the documentation and/or other materials provided # with the distribution. # # * Neither the name of the FIZSH nor the names of its contributors # may be used to endorse or promote products derived from this # software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # ############################################################################## #----------------------------------------------------------------------------- # configuration variables #----------------------------------------------------------------------------- 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' #----------------------------------------------------------------------------- # the main ZLE widgets #----------------------------------------------------------------------------- function history-substring-search-up() { _history-substring-search-begin _history-substring-search-up-history || _history-substring-search-up-buffer || _history-substring-search-up-search _history-substring-search-end } function history-substring-search-down() { _history-substring-search-begin _history-substring-search-down-history || _history-substring-search-down-buffer || _history-substring-search-down-search _history-substring-search-end } zle -N history-substring-search-up zle -N history-substring-search-down bindkey '\e[A' history-substring-search-up bindkey '\e[B' history-substring-search-down #----------------------------------------------------------------------------- # implementation details #----------------------------------------------------------------------------- setopt extendedglob zmodload -F zsh/parameter # # We have to "override" some keys and widgets if the # zsh-syntax-highlighting plugin has not been loaded: # # https://github.com/nicoulaj/zsh-syntax-highlighting # if [[ $+functions[_zsh_highlight] -eq 0 ]]; then # # Dummy implementation of _zsh_highlight() # that simply removes existing highlights # function _zsh_highlight() { region_highlight=() } # # Remove existing highlights when the user # inserts printable characters into $BUFFER # function ordinary-key-press() { if [[ $KEYS == [[:print:]] ]]; then region_highlight=() fi zle .self-insert } zle -N self-insert ordinary-key-press # # Override ZLE widgets to invoke _zsh_highlight() # # https://github.com/nicoulaj/zsh-syntax-highlighting/blob/ # bb7fcb79fad797a40077bebaf6f4e4a93c9d8163/zsh-syntax-highlighting.zsh#L121 # #--------------8<-------------------8<-------------------8<----------------- # # Copyright (c) 2010-2011 zsh-syntax-highlighting contributors # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # * Neither the name of the zsh-syntax-highlighting contributors nor the # names of its contributors may be used to endorse or promote products # derived from this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS # IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, # THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # Load ZSH module zsh/zleparameter, needed to override user defined widgets. zmodload zsh/zleparameter 2>/dev/null || { echo 'zsh-syntax-highlighting: failed loading zsh/zleparameter, exiting.' >&2 return -1 } # Override ZLE widgets to make them invoke _zsh_highlight. for event in ${${(f)"$(zle -la)"}:#(_*|orig-*|.run-help|.which-command)}; do if [[ "$widgets[$event]" == completion:* ]]; then eval "zle -C orig-$event ${${${widgets[$event]}#*:}/:/ } ; $event() { builtin zle orig-$event && _zsh_highlight } ; zle -N $event" else case $event in accept-and-menu-complete) eval "$event() { builtin zle .$event && _zsh_highlight } ; zle -N $event" ;; # The following widgets should NOT remove any previously # applied highlighting. Therefore we do not remap them. .forward-char|.backward-char|.up-line-or-history|.down-line-or-history) ;; .*) clean_event=$event[2,${#event}] # Remove the leading dot in the event name case ${widgets[$clean_event]-} in (completion|user):*) ;; *) eval "$clean_event() { builtin zle $event && _zsh_highlight } ; zle -N $clean_event" ;; esac ;; *) ;; esac fi done unset event clean_event #-------------->8------------------->8------------------->8----------------- fi function _history-substring-search-begin() { _history_substring_search_move_cursor_eol=false _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 [[ -z $BUFFER || $BUFFER != $_history_substring_search_result ]]; then # # For the purpose of highlighting we will also keep # a version without doubly-escaped meta characters. # _history_substring_search_query=$BUFFER # # $BUFFER contains the text that is in the command-line currently. # we put an extra "\\" before meta characters such as "\(" and "\)", # so that they become "\\\(" and "\\\)". # _history_substring_search_query_escaped=${BUFFER//(#m)[\][()|\\*?#<>~^]/\\$MATCH} # # Find all occurrences of the search query in the history file. # # (k) turns it an array of line numbers. # # (on) seems to remove duplicates, which are default # options. They can be turned off by (ON). # _history_substring_search_matches=(${(kon)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]. # _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. # # If $_history_substring_search_match_index is equal to 0, this indicates # that we are beyond the end 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. # # 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. # if [[ $WIDGET == history-substring-search-down ]]; then _history_substring_search_match_index=$_history_substring_search_matches_count else _history_substring_search_match_index=$_history_substring_search_matches_count_plus fi fi } function _history-substring-search-end() { _history_substring_search_result=$BUFFER # move the cursor to the end of the command line if [[ $_history_substring_search_move_cursor_eol == true ]]; then CURSOR=${#BUFFER} fi # highlight command line using zsh-syntax-highlighting _zsh_highlight # highlight the search query inside the command line if [[ -n $_history_substring_search_query_highlight && -n $_history_substring_search_query ]]; then # # 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. # : ${(S)BUFFER##(#m$HISTORY_SUBSTRING_SEARCH_GLOBBING_FLAGS)($_history_substring_search_query##)} local begin=$(( MBEGIN - 1 )) local end=$(( begin + $#_history_substring_search_query )) region_highlight+=("$begin $end $_history_substring_search_query_highlight") fi # For debugging purposes: # zle -R "mn: "$_history_substring_search_match_index" m#: "${#_history_substring_search_matches} # read -k -t 200 && zle -U $REPLY # Exit successfully from the history-substring-search-* widgets. true } function _history-substring-search-up-buffer() { # # Check if the UP arrow was pressed to move the cursor within a multi-line # buffer. This amounts to three tests: # # 1. $#buflines -gt 1. # # 2. $CURSOR -ne $#BUFFER. # # 3. Check if we are on the first line of the current multi-line buffer. # If so, pressing UP would amount to leaving the multi-line buffer. # # We check this by adding an extra "x" to $LBUFFER, which makes # sure that xlbuflines is always equal to the number of lines # until $CURSOR (including the line with the cursor on it). # local buflines XLBUFFER xlbuflines buflines=(${(f)BUFFER}) XLBUFFER=$LBUFFER"x" xlbuflines=(${(f)XLBUFFER}) if [[ $#buflines -gt 1 && $CURSOR -ne $#BUFFER && $#xlbuflines -ne 1 ]]; then zle up-line-or-history return true fi false } function _history-substring-search-down-buffer() { # # Check if the DOWN arrow was pressed to move the cursor within a multi-line # buffer. This amounts to three tests: # # 1. $#buflines -gt 1. # # 2. $CURSOR -ne $#BUFFER. # # 3. Check if we are on the last line of the current multi-line buffer. # If so, pressing DOWN would amount to leaving the multi-line buffer. # # We check this by adding an extra "x" to $RBUFFER, which makes # sure that xrbuflines is always equal to the number of lines # from $CURSOR (including the line with the cursor on it). # local buflines XRBUFFER xrbuflines buflines=(${(f)BUFFER}) XRBUFFER="x"$RBUFFER xrbuflines=(${(f)XRBUFFER}) if [[ $#buflines -gt 1 && $CURSOR -ne $#BUFFER && $#xrbuflines -ne 1 ]]; then zle down-line-or-history return true fi false } function _history-substring-search-up-history() { # # Behave like up in ZSH, except clear the $BUFFER # when beginning of history is reached like in Fish. # if [[ -z $_history_substring_search_query ]]; then # we have reached the absolute top of history if [[ $HISTNO -eq 1 ]]; then BUFFER= # going up from somewhere below the top of history else zle up-history fi return true fi false } function _history-substring-search-down-history() { # # Behave like down-history in ZSH, except clear the # $BUFFER when end of history is reached like in Fish. # if [[ -z $_history_substring_search_query ]]; then # going down from the absolute top of history if [[ $HISTNO -eq 1 && -z $BUFFER ]]; then BUFFER=${history[1]} _history_substring_search_move_cursor_eol=true # going down from somewhere above the bottom of history else zle down-history fi return true fi false } function _history-substring-search-up-search() { _history_substring_search_move_cursor_eol=true # # Highlight matches during history-substring-up-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 # $_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. # # 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. # if [[ $_history_substring_search_match_index -ge 2 ]]; then # # Highlight the next match: # # 1. Decrease the value of $_history_substring_search_match_index. # # 2. Use $HISTORY_SUBSTRING_SEARCH_HIGHLIGHT_FOUND # to highlight the current buffer. # (( _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 elif [[ $_history_substring_search_match_index -eq 1 ]]; then # # We will move beyond the end of $_history_substring_search_matches: # # 1. Decrease 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. # # 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_old_buffer=$BUFFER BUFFER=$_history_substring_search_query _history_substring_search_query_highlight=$HISTORY_SUBSTRING_SEARCH_HIGHLIGHT_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 fi } function _history-substring-search-down-search() { _history_substring_search_move_cursor_eol=true # # Highlight matches during history-substring-up-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 # $_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. # # 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. # if [[ $_history_substring_search_match_index -le $_history_substring_search_matches_count_sans ]]; then # # Highlight the next match: # # 1. Increase $_history_substring_search_match_index by 1. # # 2. Use $HISTORY_SUBSTRING_SEARCH_HIGHLIGHT_FOUND # to highlight the current buffer. # (( _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 elif [[ $_history_substring_search_match_index -eq $_history_substring_search_matches_count ]]; then # # We will move beyond the beginning of $_history_substring_search_matches: # # 1. Increase $_history_substring_search_match_index by 1. # # 2. Save the current buffer in $_history_substring_search_old_buffer, so # that it can be retrieved by _history-substring-search-up-search() # later. # # 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_old_buffer=$BUFFER BUFFER=$_history_substring_search_query _history_substring_search_query_highlight=$HISTORY_SUBSTRING_SEARCH_HIGHLIGHT_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 fi } # -*- mode: zsh; sh-indentation: 2; indent-tabs-mode: nil; sh-basic-offset: 2; -*- # vim: ft=zsh sw=2 ts=2 et