ohmyzsh/plugins/z/z.plugin.zsh
Mahesh Subramanian a4f13aec98 fix(z): prevent stack overflow from recursive Tab binding on re-source
The ZLE widget added in #13710 saves the current Tab binding and
delegates to it. If the plugin is sourced more than once, the saved
binding points to the widget itself, causing infinite recursion and
"maximum nested function level reached" errors on any Tab press.

Guard against self-reference by only capturing the binding on first
load and falling back to expand-or-complete if the saved value is
our own widget.

Fixes #13714

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-27 12:53:33 -07:00

1096 lines
34 KiB
Bash

################################################################################
# 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-2026 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
# An unusual case: if inside Docker container where datafile could be bind
# mounted
if [[ -f '/.dockerenv' || ( -r '/proc/1/cgroup' && "$(< '/proc/1/cgroup')" == *docker* ) ]]; then
print "$(< "$tempfile")" > "$datafile" 2> /dev/null
${ZSHZ[RM]} -f "$tempfile"
# All other cases
else
${ZSHZ[MV]} "$tempfile" "$datafile" 2> /dev/null ||
${ZSHZ[RM]} -f "$tempfile"
fi
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[@]}" )
# Save the existing Tab binding (only on first load to avoid self-reference
# if the plugin is sourced more than once)
if [[ -z ${ZSHZ[TAB_BINDING]} ]] || \
[[ ${ZSHZ[TAB_BINDING]} == _zshz_zle_completion_widget ]]; then
ZSHZ[TAB_BINDING]="${$(bindkey -M main '^I')##* }"
# If we somehow captured our own widget (race condition), fall back to default
[[ ${ZSHZ[TAB_BINDING]} == _zshz_zle_completion_widget ]] && \
ZSHZ[TAB_BINDING]=expand-or-complete
fi
############################################################
# ZLE widget to fix spaces-as-wildcards completion
#
# When completing a Zsh-z command with multiple search terms
# (e.g. `z us lo bi'), collapse the terms into a single
# wildcard-joined word (e.g. `z us*lo*bi') before triggering
# completion. This causes compadd to replace the whole query
# with the matched path rather than just the last word.
#
# Globals:
# ZSHZ_CMD
############################################################
_zshz_zle_completion_widget() {
emulate -L zsh
local cmd=${ZSHZ_CMD:-${_Z_CMD:-z}}
# If a trailing space was added after an already-completed absolute path
# (e.g. `z /usr/local/bin '), a second Tab would otherwise re-trigger
# completion on an empty word and insert a duplicate. Bail out early.
if [[ $LBUFFER[-1] == ' ' && ${${LBUFFER% }##* } == [/~]* ]]; then
return
fi
# Only act when there are at least two words after the command
if [[ $LBUFFER == ${cmd}\ *\ * ]]; then
local after=${LBUFFER#${cmd} }
local -a parts flag_parts search_parts
local p past_flags=0
parts=( ${(z)after} )
for p in $parts; do
if (( ! past_flags )) && [[ $p == -[cehlrRtx]## ]]; then
flag_parts+=( $p )
else
past_flags=1
search_parts+=( $p )
fi
done
if (( ${#search_parts} > 1 )); then
LBUFFER="${cmd}${flag_parts:+ ${(j: :)flag_parts}} ${(j:*:)search_parts}"
fi
fi
# If Tab had a non-default binding, continue to use it; otherwise the default
# expand-or-complete gets used.
zle ${ZSHZ[TAB_BINDING]:-expand-or-complete}
}
zle -N _zshz_zle_completion_widget
bindkey -M main '^I' _zshz_zle_completion_widget
############################################################
# 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
_zshz_zle_completion_widget'
############################################################
# 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
zle -D _zshz_zle_completion_widget
# Only restore Tab binding if it is still bound to our widget; otherwise
# leave it alone.
local _zshz_current_tab
_zshz_current_tab="$(bindkey -M main '^I' 2>/dev/null || true)"
if [[ ${_zshz_current_tab##* } == _zshz_zle_completion_widget ]]; then
bindkey -M main '^I' "${ZSHZ[TAB_BINDING]:-expand-or-complete}"
fi
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: