################################################################################ # Zsh-z - jump around with Zsh - A native Zsh version of z without awk, sort, # date, or sed # # https://github.com/agkozak/zsh-z # # Copyright (c) 2018-2023 Alexandros Kozak # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # # z (https://github.com/rupa/z) is copyright (c) 2009 rupa deadwyler and # licensed under the WTFPL license, Version 2. # # Zsh-z maintains a jump-list of the directories you actually use. # # INSTALL: # * put something like this in your .zshrc: # source /path/to/zsh-z.plugin.zsh # * cd around for a while to build up the database # # USAGE: # * z foo cd to the most frecent directory matching foo # * z foo bar cd to the most frecent directory matching both foo and bar # (e.g. /foo/bat/bar/quux) # * z -r foo cd to the highest ranked directory matching foo # * z -t foo cd to most recently accessed directory matching foo # * z -l foo List matches instead of changing directories # * z -e foo Echo the best match without changing directories # * z -c foo Restrict matches to subdirectories of PWD # * z -x Remove a directory (default: PWD) from the database # * z -xR Remove a directory (default: PWD) and its subdirectories from # the database # # ENVIRONMENT VARIABLES: # # ZSHZ_CASE -> if `ignore', pattern matching is case-insensitive; if `smart', # pattern matching is case-insensitive only when the pattern is all # lowercase # ZSHZ_CD -> the directory-changing command that is used (default: builtin cd) # ZSHZ_CMD -> name of command (default: z) # ZSHZ_COMPLETION -> completion method (default: 'frecent'; 'legacy' for # alphabetic sorting) # ZSHZ_DATA -> name of datafile (default: ~/.z) # ZSHZ_EXCLUDE_DIRS -> array of directories to exclude from your database # (default: empty) # ZSHZ_KEEP_DIRS -> array of directories that should not be removed from the # database, even if they are not currently available (default: empty) # ZSHZ_MAX_SCORE -> maximum combined score the database entries can have # before beginning to age (default: 9000) # ZSHZ_NO_RESOLVE_SYMLINKS -> '1' prevents symlink resolution # ZSHZ_OWNER -> your username (if you want use Zsh-z while using sudo -s) # ZSHZ_UNCOMMON -> if 1, do not jump to "common directories," but rather drop # subdirectories based on what the search string was (default: 0) ################################################################################ autoload -U is-at-least if ! is-at-least 4.3.11; then print "Zsh-z requires Zsh v4.3.11 or higher." >&2 && exit fi ############################################################ # The help message # # Globals: # ZSHZ_CMD ############################################################ _zshz_usage() { print "Usage: ${ZSHZ_CMD:-${_Z_CMD:-z}} [OPTION]... [ARGUMENT] Jump to a directory that you have visited frequently or recently, or a bit of both, based on the partial string ARGUMENT. With no ARGUMENT, list the directory history in ascending rank. --add Add a directory to the database -c Only match subdirectories of the current directory -e Echo the best match without going to it -h Display this help and exit -l List all matches without going to them -r Match by rank -t Match by recent access -x Remove a directory from the database (by default, the current directory) -xR Remove a directory and its subdirectories from the database (by default, the current directory)" | fold -s -w $COLUMNS >&2 } # Load zsh/datetime module, if necessary (( ${+EPOCHSECONDS} )) || zmodload zsh/datetime # Global associative array for internal use typeset -gA ZSHZ # Fallback utilities in case Zsh lacks zsh/files (as is the case with MobaXterm) ZSHZ[CHOWN]='chown' ZSHZ[MV]='mv' ZSHZ[RM]='rm' # Try to load zsh/files utilities if [[ ${builtins[zf_chown]-} != 'defined' || ${builtins[zf_mv]-} != 'defined' || ${builtins[zf_rm]-} != 'defined' ]]; then zmodload -F zsh/files b:zf_chown b:zf_mv b:zf_rm &> /dev/null fi # Use zsh/files, if it is available [[ ${builtins[zf_chown]-} == 'defined' ]] && ZSHZ[CHOWN]='zf_chown' [[ ${builtins[zf_mv]-} == 'defined' ]] && ZSHZ[MV]='zf_mv' [[ ${builtins[zf_rm]-} == 'defined' ]] && ZSHZ[RM]='zf_rm' # Load zsh/system, if necessary [[ ${modules[zsh/system]-} == 'loaded' ]] || zmodload zsh/system &> /dev/null # Make sure ZSHZ_EXCLUDE_DIRS has been declared so that other scripts can # simply append to it (( ${+ZSHZ_EXCLUDE_DIRS} )) || typeset -gUa ZSHZ_EXCLUDE_DIRS # Determine if zsystem flock is available zsystem supports flock &> /dev/null && ZSHZ[USE_FLOCK]=1 # Determine if `print -v' is supported is-at-least 5.3.0 && ZSHZ[PRINTV]=1 ############################################################ # The Zsh-z Command # # Globals: # ZSHZ # ZSHZ_CASE # ZSHZ_CD # ZSHZ_COMPLETION # ZSHZ_DATA # ZSHZ_DEBUG # ZSHZ_EXCLUDE_DIRS # ZSHZ_KEEP_DIRS # ZSHZ_MAX_SCORE # ZSHZ_OWNER # # Arguments: # $* Command options and arguments ############################################################ zshz() { # Don't use `emulate -L zsh' - it breaks PUSHD_IGNORE_DUPS setopt LOCAL_OPTIONS NO_KSH_ARRAYS NO_SH_WORD_SPLIT EXTENDED_GLOB UNSET (( ZSHZ_DEBUG )) && setopt LOCAL_OPTIONS WARN_CREATE_GLOBAL local REPLY local -a lines # Allow the user to specify a custom datafile in $ZSHZ_DATA (or legacy $_Z_DATA) local custom_datafile="${ZSHZ_DATA:-$_Z_DATA}" # If a datafile was provided as a standalone file without a directory path # print a warning and exit if [[ -n ${custom_datafile} && ${custom_datafile} != */* ]]; then print "ERROR: You configured a custom Zsh-z datafile (${custom_datafile}), but have not specified its directory." >&2 exit fi # If the user specified a datafile, use that or default to ~/.z # If the datafile is a symlink, it gets dereferenced local datafile=${${custom_datafile:-$HOME/.z}:A} # If the datafile is a directory, print a warning and exit if [[ -d $datafile ]]; then print "ERROR: Zsh-z's datafile (${datafile}) is a directory." >&2 exit fi # Make sure that the datafile exists before attempting to read it or lock it # for writing [[ -f $datafile ]] || { mkdir -p "${datafile:h}" && touch "$datafile" } # Bail if we don't own the datafile and $ZSHZ_OWNER is not set [[ -z ${ZSHZ_OWNER:-${_Z_OWNER}} && -f $datafile && ! -O $datafile ]] && return # Load the datafile into an array and parse it lines=( ${(f)"$(< $datafile)"} ) # Discard entries that are incomplete or incorrectly formatted lines=( ${(M)lines:#/*\|[[:digit:]]##[.,]#[[:digit:]]#\|[[:digit:]]##} ) ############################################################ # Add a path to or remove one from the datafile # # Globals: # ZSHZ # ZSHZ_EXCLUDE_DIRS # ZSHZ_OWNER # # Arguments: # $1 Which action to perform (--add/--remove) # $2 The path to add ############################################################ _zshz_add_or_remove_path() { local action=${1} shift if [[ $action == '--add' ]]; then # TODO: The following tasks are now handled by _agkozak_precmd. Dead code? # Don't add $HOME [[ $* == $HOME ]] && return # Don't track directory trees excluded in ZSHZ_EXCLUDE_DIRS local exclude for exclude in ${(@)ZSHZ_EXCLUDE_DIRS:-${(@)_Z_EXCLUDE_DIRS}}; do case $* in ${exclude}|${exclude}/*) return ;; esac done fi # A temporary file that gets copied over the datafile if all goes well local tempfile="${datafile}.${RANDOM}" # See https://github.com/rupa/z/pull/199/commits/ed6eeed9b70d27c1582e3dd050e72ebfe246341c if (( ZSHZ[USE_FLOCK] )); then local lockfd # Grab exclusive lock (released when function exits) zsystem flock -f lockfd "$datafile" 2> /dev/null || return fi integer tmpfd case $action in --add) exec {tmpfd}>|"$tempfile" # Open up tempfile for writing _zshz_update_datafile $tmpfd "$*" local ret=$? ;; --remove) local xdir # Directory to be removed if (( ${ZSHZ_NO_RESOLVE_SYMLINKS:-${_Z_NO_RESOLVE_SYMLINKS}} )); then [[ -d ${${*:-${PWD}}:a} ]] && xdir=${${*:-${PWD}}:a} else [[ -d ${${*:-${PWD}}:A} ]] && xdir=${${*:-${PWD}}:a} fi local -a lines_to_keep if (( ${+opts[-R]} )); then # Prompt user before deleting entire database if [[ $xdir == '/' ]] && ! read -q "?Delete entire Zsh-z database? "; then print && return 1 fi # All of the lines that don't match the directory to be deleted lines_to_keep=( ${lines:#${xdir}\|*} ) # Or its subdirectories lines_to_keep=( ${lines_to_keep:#${xdir%/}/**} ) else # All of the lines that don't match the directory to be deleted lines_to_keep=( ${lines:#${xdir}\|*} ) fi if [[ $lines != "$lines_to_keep" ]]; then lines=( $lines_to_keep ) else return 1 # The $PWD isn't in the datafile fi exec {tmpfd}>|"$tempfile" # Open up tempfile for writing print -u $tmpfd -l -- $lines local ret=$? ;; esac if (( tmpfd != 0 )); then # Close tempfile exec {tmpfd}>&- fi if (( ret != 0 )); then # Avoid clobbering the datafile if the write to tempfile failed ${ZSHZ[RM]} -f "$tempfile" return $ret fi local owner owner=${ZSHZ_OWNER:-${_Z_OWNER}} if (( ZSHZ[USE_FLOCK] )); then ${ZSHZ[MV]} "$tempfile" "$datafile" 2> /dev/null || ${ZSHZ[RM]} -f "$tempfile" if [[ -n $owner ]]; then ${ZSHZ[CHOWN]} ${owner}:"$(id -ng ${owner})" "$datafile" fi else if [[ -n $owner ]]; then ${ZSHZ[CHOWN]} "${owner}":"$(id -ng "${owner}")" "$tempfile" fi ${ZSHZ[MV]} -f "$tempfile" "$datafile" 2> /dev/null || ${ZSHZ[RM]} -f "$tempfile" fi # In order to make z -x work, we have to disable zsh-z's adding # to the database until the user changes directory and the # chpwd_functions are run if [[ $action == '--remove' ]]; then ZSHZ[DIRECTORY_REMOVED]=1 fi } ############################################################ # Read the current datafile contents, update them, "age" them # when the total rank gets high enough, and print the new # contents to STDOUT. # # Globals: # ZSHZ_KEEP_DIRS # ZSHZ_MAX_SCORE # # Arguments: # $1 File descriptor linked to tempfile # $2 Path to be added to datafile ############################################################ _zshz_update_datafile() { integer fd=$1 local -A rank time # Characters special to the shell (such as '[]') are quoted with backslashes # See https://github.com/rupa/z/issues/246 local add_path=${(q)2} local -a existing_paths local now=$EPOCHSECONDS line dir local path_field rank_field time_field count x rank[$add_path]=1 time[$add_path]=$now # Remove paths from database if they no longer exist for line in $lines; do if [[ ! -d ${line%%\|*} ]]; then for dir in ${(@)ZSHZ_KEEP_DIRS}; do if [[ ${line%%\|*} == ${dir}/* || ${line%%\|*} == $dir || $dir == '/' ]]; then existing_paths+=( $line ) fi done else existing_paths+=( $line ) fi done lines=( $existing_paths ) for line in $lines; do path_field=${(q)line%%\|*} rank_field=${${line%\|*}#*\|} time_field=${line##*\|} # When a rank drops below 1, drop the path from the database (( rank_field < 1 )) && continue if [[ $path_field == $add_path ]]; then rank[$path_field]=$rank_field (( rank[$path_field]++ )) time[$path_field]=$now else rank[$path_field]=$rank_field time[$path_field]=$time_field fi (( count += rank_field )) done if (( count > ${ZSHZ_MAX_SCORE:-${_Z_MAX_SCORE:-9000}} )); then # Aging for x in ${(k)rank}; do print -u $fd -- "$x|$(( 0.99 * rank[$x] ))|${time[$x]}" || return 1 done else for x in ${(k)rank}; do print -u $fd -- "$x|${rank[$x]}|${time[$x]}" || return 1 done fi } ############################################################ # The original tab completion method # # String processing is smartcase -- case-insensitive if the # search string is lowercase, case-sensitive if there are # any uppercase letters. Spaces in the search string are # treated as *'s in globbing. Read the contents of the # datafile and print matches to STDOUT. # # Arguments: # $1 The string to be completed ############################################################ _zshz_legacy_complete() { local line path_field path_field_normalized # Replace spaces in the search string with asterisks for globbing 1=${1//[[:space:]]/*} for line in $lines; do path_field=${line%%\|*} path_field_normalized=$path_field if (( ZSHZ_TRAILING_SLASH )); then path_field_normalized=${path_field%/}/ fi # If the search string is all lowercase, the search will be case-insensitive if [[ $1 == "${1:l}" && ${path_field_normalized:l} == *${~1}* ]]; then print -- $path_field # Otherwise, case-sensitive elif [[ $path_field_normalized == *${~1}* ]]; then print -- $path_field fi done # TODO: Search strings with spaces in them are currently treated case- # insensitively. } ############################################################ # `print' or `printf' to REPLY # # Variable assignment through command substitution, of the # form # # foo=$( bar ) # # requires forking a subshell; on Cygwin/MSYS2/WSL1 that can # be surprisingly slow. Zsh-z avoids doing that by printing # values to the variable REPLY. Since Zsh v5.3.0 that has # been possible with `print -v'; for earlier versions of the # shell, the values are placed on the editing buffer stack # and then `read' into REPLY. # # Globals: # ZSHZ # # Arguments: # Options and parameters for `print' ############################################################ _zshz_printv() { # NOTE: For a long time, ZSH's `print -v' had a tendency # to mangle multibyte strings: # # https://www.zsh.org/mla/workers/2020/msg00307.html # # The bug was fixed in late 2020: # # https://github.com/zsh-users/zsh/commit/b6ba74cd4eaec2b6cb515748cf1b74a19133d4a4#diff-32bbef18e126b837c87b06f11bfc61fafdaa0ed99fcb009ec53f4767e246b129 # # In order to support shells with the bug, we must use a form of `printf`, # which does not exhibit the undesired behavior. See # # https://www.zsh.org/mla/workers/2020/msg00308.html if (( ZSHZ[PRINTV] )); then builtin print -v REPLY -f %s $@ else builtin print -z $@ builtin read -rz REPLY fi } ############################################################ # If matches share a common root, find it, and put it in # REPLY for _zshz_output to use. # # Arguments: # $1 Name of associative array of matches and ranks ############################################################ _zshz_find_common_root() { local -a common_matches local x short common_matches=( ${(@Pk)1} ) for x in ${(@)common_matches}; do if [[ -z $short ]] || (( $#x < $#short )) || [[ $x != ${short}/* ]]; then short=$x fi done [[ $short == '/' ]] && return for x in ${(@)common_matches}; do [[ $x != $short* ]] && return done _zshz_printv -- $short } ############################################################ # Calculate a common root, if there is one. Then do one of # the following: # # 1) Print a list of completions in frecent order; # 2) List them (z -l) to STDOUT; or # 3) Put a common root or best match into REPLY # # Globals: # ZSHZ_UNCOMMON # # Arguments: # $1 Name of an associative array of matches and ranks # $2 The best match or best case-insensitive match # $3 Whether to produce a completion, a list, or a root or # match ############################################################ _zshz_output() { local match_array=$1 match=$2 format=$3 local common k x local -a descending_list output local -A output_matches output_matches=( ${(Pkv)match_array} ) _zshz_find_common_root $match_array common=$REPLY case $format in completion) for k in ${(@k)output_matches}; do _zshz_printv -f "%.2f|%s" ${output_matches[$k]} $k descending_list+=( ${(f)REPLY} ) REPLY='' done descending_list=( ${${(@On)descending_list}#*\|} ) print -l $descending_list ;; list) local path_to_display for x in ${(k)output_matches}; do if (( ${output_matches[$x]} )); then path_to_display=$x (( ZSHZ_TILDE )) && path_to_display=${path_to_display/#${HOME}/\~} _zshz_printv -f "%-10d %s\n" ${output_matches[$x]} $path_to_display output+=( ${(f)REPLY} ) REPLY='' fi done if [[ -n $common ]]; then (( ZSHZ_TILDE )) && common=${common/#${HOME}/\~} (( $#output > 1 )) && printf "%-10s %s\n" 'common:' $common fi # -lt if (( $+opts[-t] )); then for x in ${(@On)output}; do print -- $x done # -lr elif (( $+opts[-r] )); then for x in ${(@on)output}; do print -- $x done # -l else for x in ${(@on)output}; do print $x done fi ;; *) if (( ! ZSHZ_UNCOMMON )) && [[ -n $common ]]; then _zshz_printv -- $common else _zshz_printv -- ${(P)match} fi ;; esac } ############################################################ # Match a pattern by rank, time, or a combination of the # two, and output the results as completions, a list, or a # best match. # # Globals: # ZSHZ # ZSHZ_CASE # ZSHZ_KEEP_DIRS # ZSHZ_OWNER # # Arguments: # #1 Pattern to match # $2 Matching method (rank, time, or [default] frecency) # $3 Output format (completion, list, or [default] store # in REPLY ############################################################ _zshz_find_matches() { setopt LOCAL_OPTIONS NO_EXTENDED_GLOB local fnd=$1 method=$2 format=$3 local -a existing_paths local line dir path_field rank_field time_field rank dx escaped_path_field local -A matches imatches local best_match ibest_match hi_rank=-9999999999 ihi_rank=-9999999999 # Remove paths from database if they no longer exist for line in $lines; do if [[ ! -d ${line%%\|*} ]]; then for dir in ${(@)ZSHZ_KEEP_DIRS}; do if [[ ${line%%\|*} == ${dir}/* || ${line%%\|*} == $dir || $dir == '/' ]]; then existing_paths+=( $line ) fi done else existing_paths+=( $line ) fi done lines=( $existing_paths ) for line in $lines; do path_field=${line%%\|*} rank_field=${${line%\|*}#*\|} time_field=${line##*\|} case $method in rank) rank=$rank_field ;; time) (( rank = time_field - EPOCHSECONDS )) ;; *) # Frecency routine (( dx = EPOCHSECONDS - time_field )) rank=$(( 10000 * rank_field * (3.75/( (0.0001 * dx + 1) + 0.25)) )) ;; esac # Use spaces as wildcards local q=${fnd//[[:space:]]/\*} # If $ZSHZ_TRAILING_SLASH is set, use path_field with a trailing slash for matching. local path_field_normalized=$path_field if (( ZSHZ_TRAILING_SLASH )); then path_field_normalized=${path_field%/}/ fi # If $ZSHZ_CASE is 'ignore', be case-insensitive. # # If it's 'smart', be case-insensitive unless the string to be matched # includes capital letters. # # Otherwise, the default behavior of Zsh-z is to match case-sensitively if # possible, then to fall back on a case-insensitive match if possible. if [[ $ZSHZ_CASE == 'smart' && ${1:l} == $1 && ${path_field_normalized:l} == ${~q:l} ]]; then imatches[$path_field]=$rank elif [[ $ZSHZ_CASE != 'ignore' && $path_field_normalized == ${~q} ]]; then matches[$path_field]=$rank elif [[ $ZSHZ_CASE != 'smart' && ${path_field_normalized:l} == ${~q:l} ]]; then imatches[$path_field]=$rank fi # Escape characters that would cause "invalid subscript" errors # when accessing the associative array. escaped_path_field=${path_field//'\'/'\\'} escaped_path_field=${escaped_path_field//'`'/'\`'} escaped_path_field=${escaped_path_field//'('/'\('} escaped_path_field=${escaped_path_field//')'/'\)'} escaped_path_field=${escaped_path_field//'['/'\['} escaped_path_field=${escaped_path_field//']'/'\]'} if (( matches[$escaped_path_field] )) && (( matches[$escaped_path_field] > hi_rank )); then best_match=$path_field hi_rank=${matches[$escaped_path_field]} elif (( imatches[$escaped_path_field] )) && (( imatches[$escaped_path_field] > ihi_rank )); then ibest_match=$path_field ihi_rank=${imatches[$escaped_path_field]} ZSHZ[CASE_INSENSITIVE]=1 fi done # Return 1 when there are no matches [[ -z $best_match && -z $ibest_match ]] && return 1 if [[ -n $best_match ]]; then _zshz_output matches best_match $format elif [[ -n $ibest_match ]]; then _zshz_output imatches ibest_match $format fi } # THE MAIN ROUTINE local -A opts zparseopts -E -D -A opts -- \ -add \ -complete \ c \ e \ h \ -help \ l \ r \ R \ t \ x if [[ $1 == '--' ]]; then shift elif [[ -n ${(M)@:#-*} && -z $compstate ]]; then print "Improper option(s) given." _zshz_usage return 1 fi local opt output_format method='frecency' fnd prefix req for opt in ${(k)opts}; do case $opt in --add) [[ ! -d $* ]] && return 1 local dir # Cygwin and MSYS2 have a hard time with relative paths expressed from / if [[ $OSTYPE == (cygwin|msys) && $PWD == '/' && $* != /* ]]; then set -- "/$*" fi if (( ${ZSHZ_NO_RESOLVE_SYMLINKS:-${_Z_NO_RESOLVE_SYMLINKS}} )); then dir=${*:a} else dir=${*:A} fi _zshz_add_or_remove_path --add "$dir" return ;; --complete) if [[ -s $datafile && ${ZSHZ_COMPLETION:-frecent} == 'legacy' ]]; then _zshz_legacy_complete "$1" return fi output_format='completion' ;; -c) [[ $* == ${PWD}/* || $PWD == '/' ]] || prefix="$PWD " ;; -h|--help) _zshz_usage return ;; -l) output_format='list' ;; -r) method='rank' ;; -t) method='time' ;; -x) # Cygwin and MSYS2 have a hard time with relative paths expressed from / if [[ $OSTYPE == (cygwin|msys) && $PWD == '/' && $* != /* ]]; then set -- "/$*" fi _zshz_add_or_remove_path --remove $* return ;; esac done req="$*" fnd="$prefix$*" [[ -n $fnd && $fnd != "$PWD " ]] || { [[ $output_format != 'completion' ]] && output_format='list' } ######################################################### # Allow the user to specify directory-changing command # using $ZSHZ_CD (default: builtin cd). # # Globals: # ZSHZ_CD # # Arguments: # $* Path ######################################################### zshz_cd() { setopt LOCAL_OPTIONS NO_WARN_CREATE_GLOBAL if [[ -z $ZSHZ_CD ]]; then builtin cd "$*" else ${=ZSHZ_CD} "$*" fi } ######################################################### # If $ZSHZ_ECHO == 1, display paths as you jump to them. # If it is also the case that $ZSHZ_TILDE == 1, display # the home directory as a tilde. ######################################################### _zshz_echo() { if (( ZSHZ_ECHO )); then if (( ZSHZ_TILDE )); then print ${PWD/#${HOME}/\~} else print $PWD fi fi } if [[ ${@: -1} == /* ]] && (( ! $+opts[-e] && ! $+opts[-l] )); then # cd if possible; echo the new path if $ZSHZ_ECHO == 1 [[ -d ${@: -1} ]] && zshz_cd ${@: -1} && _zshz_echo && return fi # With option -c, make sure query string matches beginning of matches; # otherwise look for matches anywhere in paths # zpm-zsh/colors has a global $c, so we'll avoid math expressions here if [[ ! -z ${(tP)opts[-c]} ]]; then _zshz_find_matches "$fnd*" $method $output_format else _zshz_find_matches "*$fnd*" $method $output_format fi local ret2=$? local cd cd=$REPLY # New experimental "uncommon" behavior # # If the best choice at this point is something like /foo/bar/foo/bar, and the # search pattern is `bar', go to /foo/bar/foo/bar; but if the search pattern # is `foo', go to /foo/bar/foo if (( ZSHZ_UNCOMMON )) && [[ -n $cd ]]; then if [[ -n $cd ]]; then # In the search pattern, replace spaces with * local q=${fnd//[[:space:]]/\*} q=${q%/} # Trailing slash has to be removed # As long as the best match is not case-insensitive if (( ! ZSHZ[CASE_INSENSITIVE] )); then # Count the number of characters in $cd that $q matches local q_chars=$(( ${#cd} - ${#${cd//${~q}/}} )) # Try dropping directory elements from the right; stop when it affects # how many times the search pattern appears until (( ( ${#cd:h} - ${#${${cd:h}//${~q}/}} ) != q_chars )); do cd=${cd:h} done # If the best match is case-insensitive else local q_chars=$(( ${#cd} - ${#${${cd:l}//${~${q:l}}/}} )) until (( ( ${#cd:h} - ${#${${${cd:h}:l}//${~${q:l}}/}} ) != q_chars )); do cd=${cd:h} done fi ZSHZ[CASE_INSENSITIVE]=0 fi fi if (( ret2 == 0 )) && [[ -n $cd ]]; then if (( $+opts[-e] )); then # echo (( ZSHZ_TILDE )) && cd=${cd/#${HOME}/\~} print -- "$cd" else # cd if possible; echo the new path if $ZSHZ_ECHO == 1 [[ -d $cd ]] && zshz_cd "$cd" && _zshz_echo fi else # if $req is a valid path, cd to it; echo the new path if $ZSHZ_ECHO == 1 if ! (( $+opts[-e] || $+opts[-l] )) && [[ -d $req ]]; then zshz_cd "$req" && _zshz_echo else return $ret2 fi fi } alias ${ZSHZ_CMD:-${_Z_CMD:-z}}='zshz 2>&1' ############################################################ # precmd - add path to datafile unless `z -x' has just been # run # # Globals: # ZSHZ ############################################################ _zshz_precmd() { # Protect against `setopt NO_UNSET' setopt LOCAL_OPTIONS UNSET # Do not add PWD to datafile when in HOME directory, or # if `z -x' has just been run [[ $PWD == "$HOME" ]] || (( ZSHZ[DIRECTORY_REMOVED] )) && return # Don't track directory trees excluded in ZSHZ_EXCLUDE_DIRS local exclude for exclude in ${(@)ZSHZ_EXCLUDE_DIRS:-${(@)_Z_EXCLUDE_DIRS}}; do case $PWD in ${exclude}|${exclude}/*) return ;; esac done # It appears that forking a subshell is so slow in Windows that it is better # just to add the PWD to the datafile in the foreground if [[ $OSTYPE == (cygwin|msys) ]]; then zshz --add "$PWD" else (zshz --add "$PWD" &) fi # See https://github.com/rupa/z/pull/247/commits/081406117ea42ccb8d159f7630cfc7658db054b6 : $RANDOM } ############################################################ # chpwd # # When the $PWD is removed from the datafile with `z -x', # Zsh-z refrains from adding it again until the user has # left the directory. # # Globals: # ZSHZ ############################################################ _zshz_chpwd() { ZSHZ[DIRECTORY_REMOVED]=0 } autoload -Uz add-zsh-hook add-zsh-hook precmd _zshz_precmd add-zsh-hook chpwd _zshz_chpwd ############################################################ # Completion ############################################################ # Standardized $0 handling # https://zdharma-continuum.github.io/Zsh-100-Commits-Club/Zsh-Plugin-Standard.html 0="${${ZERO:-${0:#$ZSH_ARGZERO}}:-${(%):-%N}}" 0="${${(M)0:#/*}:-$PWD/$0}" (( ${fpath[(ie)${0:A:h}]} <= ${#fpath} )) || fpath=( "${0:A:h}" "${fpath[@]}" ) ############################################################ # zsh-z functions ############################################################ ZSHZ[FUNCTIONS]='_zshz_usage _zshz_add_or_remove_path _zshz_update_datafile _zshz_legacy_complete _zshz_printv _zshz_find_common_root _zshz_output _zshz_find_matches zshz _zshz_precmd _zshz_chpwd _zshz' ############################################################ # Enable WARN_NESTED_VAR for functions listed in # ZSHZ[FUNCTIONS] ############################################################ (( ${+ZSHZ_DEBUG} )) && () { if is-at-least 5.4.0; then local x for x in ${=ZSHZ[FUNCTIONS]}; do functions -W $x done fi } ############################################################ # Unload function # # See https://github.com/agkozak/Zsh-100-Commits-Club/blob/master/Zsh-Plugin-Standard.adoc#unload-fun # # Globals: # ZSHZ # ZSHZ_CMD ############################################################ zsh-z_plugin_unload() { emulate -L zsh add-zsh-hook -D precmd _zshz_precmd add-zsh-hook -d chpwd _zshz_chpwd local x for x in ${=ZSHZ[FUNCTIONS]}; do (( ${+functions[$x]} )) && unfunction $x done unset ZSHZ fpath=( "${(@)fpath:#${0:A:h}}" ) (( ${+aliases[${ZSHZ_CMD:-${_Z_CMD:-z}}]} )) && unalias ${ZSHZ_CMD:-${_Z_CMD:-z}} unfunction $0 } # vim: fdm=indent:ts=2:et:sts=2:sw=2: