2022-10-19 01:10:55 +08:00
################################################################################
# Zsh-z - jump around with Zsh - A native Zsh version of z without awk, sort,
# date, or sed
#
# https://github.com/agkozak/zsh-z
#
2023-04-28 15:05:36 +08:00
# Copyright (c) 2018-2023 Alexandros Kozak
2022-10-19 01:10:55 +08:00
#
# 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
2023-04-28 15:05:36 +08:00
# ZSHZ_CD -> the directory-changing command that is used (default: builtin cd)
2022-10-19 01:10:55 +08:00
# 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)
################################################################################
2021-12-19 04:53:49 +08:00
2022-10-19 01:10:55 +08:00
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
# Load zsh/files, if necessary
[ [ ${ builtins [zf_chown] } = = 'defined' &&
${ builtins [zf_mv] } = = 'defined' &&
${ builtins [zf_rm] } = = 'defined' ] ] ||
zmodload -F zsh/files b:zf_chown b:zf_mv b:zf_rm
# Load zsh/system, if necessary
[ [ ${ modules [zsh/system] } = = 'loaded' ] ] || zmodload zsh/system & > /dev/null
# Global associative array for internal use
typeset -gA ZSHZ
# 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
2023-04-28 15:05:36 +08:00
# ZSHZ_CD
2022-10-19 01:10:55 +08:00
# 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
( ( ZSHZ_DEBUG ) ) && setopt LOCAL_OPTIONS WARN_CREATE_GLOBAL
local REPLY
local -a lines
2023-04-28 15:05:36 +08:00
# 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
2022-10-19 01:10:55 +08:00
# If the datafile is a symlink, it gets dereferenced
2023-04-28 15:05:36 +08:00
local datafile = ${ ${ custom_datafile :- $HOME /.z } : A }
2022-10-19 01:10:55 +08:00
# 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
2023-04-28 15:05:36 +08:00
[ [ -f $datafile ] ] || { mkdir -p " ${ datafile : h } " && touch " $datafile " }
2022-10-19 01:10:55 +08:00
# 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
zf_rm -f " $tempfile "
return $ret
fi
local owner
owner = ${ ZSHZ_OWNER :- ${ _Z_OWNER } }
if ( ( ZSHZ[ USE_FLOCK] ) ) ; then
zf_mv " $tempfile " " $datafile " 2> /dev/null || zf_rm -f " $tempfile "
if [ [ -n $owner ] ] ; then
zf_chown ${ owner } :" $( id -ng ${ owner } ) " " $datafile "
fi
else
if [ [ -n $owner ] ] ; then
zf_chown " ${ owner } " :" $( id -ng " ${ owner } " ) " " $tempfile "
fi
zf_mv -f " $tempfile " " $datafile " 2> /dev/null || zf_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 curent 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 ) )
2023-04-28 15:05:36 +08:00
rank = $(( 10000 * rank_field * ( 3 .75/( ( 0 .0001 * dx + 1 ) + 0 .25)) ) )
2022-10-19 01:10:55 +08:00
; ;
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'
}
2023-04-28 15:05:36 +08:00
#########################################################
# 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
}
2022-10-19 01:10:55 +08:00
#########################################################
# 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
2023-04-28 15:05:36 +08:00
[ [ -d ${ @ : -1 } ] ] && zshz_cd ${ @ : -1 } && _zshz_echo && return
2022-10-19 01:10:55 +08:00
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
2023-04-28 15:05:36 +08:00
[ [ -d $cd ] ] && zshz_cd " $cd " && _zshz_echo
2022-10-19 01:10:55 +08:00
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
2023-04-28 15:05:36 +08:00
zshz_cd " $req " && _zshz_echo
2022-10-19 01:10:55 +08:00
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( ) {
# 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
############################################################
# Standarized $0 handling
2023-04-28 15:05:36 +08:00
# https://zdharma-continuum.github.io/Zsh-100-Commits-Club/Zsh-Plugin-Standard.html
0 = " ${ ${ ZERO :- ${ 0 : # $ZSH_ARGZERO } } :- ${ (%) :- %N } } "
0 = " ${ ${ (M)0 : #/* } :- $PWD / $0 } "
2022-10-19 01:10:55 +08:00
( ( ${ 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: