z.plugin.zsh 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014
  1. ################################################################################
  2. # Zsh-z - jump around with Zsh - A native Zsh version of z without awk, sort,
  3. # date, or sed
  4. #
  5. # https://github.com/agkozak/zsh-z
  6. #
  7. # Copyright (c) 2018-2023 Alexandros Kozak
  8. #
  9. # Permission is hereby granted, free of charge, to any person obtaining a copy
  10. # of this software and associated documentation files (the "Software"), to deal
  11. # in the Software without restriction, including without limitation the rights
  12. # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  13. # copies of the Software, and to permit persons to whom the Software is
  14. # furnished to do so, subject to the following conditions:
  15. #
  16. # The above copyright notice and this permission notice shall be included in all
  17. # copies or substantial portions of the Software.
  18. #
  19. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  20. # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  21. # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  22. # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  23. # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  24. # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  25. # SOFTWARE.
  26. #
  27. # z (https://github.com/rupa/z) is copyright (c) 2009 rupa deadwyler and
  28. # licensed under the WTFPL license, Version 2.
  29. #
  30. # Zsh-z maintains a jump-list of the directories you actually use.
  31. #
  32. # INSTALL:
  33. # * put something like this in your .zshrc:
  34. # source /path/to/zsh-z.plugin.zsh
  35. # * cd around for a while to build up the database
  36. #
  37. # USAGE:
  38. # * z foo cd to the most frecent directory matching foo
  39. # * z foo bar cd to the most frecent directory matching both foo and bar
  40. # (e.g. /foo/bat/bar/quux)
  41. # * z -r foo cd to the highest ranked directory matching foo
  42. # * z -t foo cd to most recently accessed directory matching foo
  43. # * z -l foo List matches instead of changing directories
  44. # * z -e foo Echo the best match without changing directories
  45. # * z -c foo Restrict matches to subdirectories of PWD
  46. # * z -x Remove a directory (default: PWD) from the database
  47. # * z -xR Remove a directory (default: PWD) and its subdirectories from
  48. # the database
  49. #
  50. # ENVIRONMENT VARIABLES:
  51. #
  52. # ZSHZ_CASE -> if `ignore', pattern matching is case-insensitive; if `smart',
  53. # pattern matching is case-insensitive only when the pattern is all
  54. # lowercase
  55. # ZSHZ_CD -> the directory-changing command that is used (default: builtin cd)
  56. # ZSHZ_CMD -> name of command (default: z)
  57. # ZSHZ_COMPLETION -> completion method (default: 'frecent'; 'legacy' for
  58. # alphabetic sorting)
  59. # ZSHZ_DATA -> name of datafile (default: ~/.z)
  60. # ZSHZ_EXCLUDE_DIRS -> array of directories to exclude from your database
  61. # (default: empty)
  62. # ZSHZ_KEEP_DIRS -> array of directories that should not be removed from the
  63. # database, even if they are not currently available (default: empty)
  64. # ZSHZ_MAX_SCORE -> maximum combined score the database entries can have
  65. # before beginning to age (default: 9000)
  66. # ZSHZ_NO_RESOLVE_SYMLINKS -> '1' prevents symlink resolution
  67. # ZSHZ_OWNER -> your username (if you want use Zsh-z while using sudo -s)
  68. # ZSHZ_UNCOMMON -> if 1, do not jump to "common directories," but rather drop
  69. # subdirectories based on what the search string was (default: 0)
  70. ################################################################################
  71. autoload -U is-at-least
  72. if ! is-at-least 4.3.11; then
  73. print "Zsh-z requires Zsh v4.3.11 or higher." >&2 && exit
  74. fi
  75. ############################################################
  76. # The help message
  77. #
  78. # Globals:
  79. # ZSHZ_CMD
  80. ############################################################
  81. _zshz_usage() {
  82. print "Usage: ${ZSHZ_CMD:-${_Z_CMD:-z}} [OPTION]... [ARGUMENT]
  83. Jump to a directory that you have visited frequently or recently, or a bit of both, based on the partial string ARGUMENT.
  84. With no ARGUMENT, list the directory history in ascending rank.
  85. --add Add a directory to the database
  86. -c Only match subdirectories of the current directory
  87. -e Echo the best match without going to it
  88. -h Display this help and exit
  89. -l List all matches without going to them
  90. -r Match by rank
  91. -t Match by recent access
  92. -x Remove a directory from the database (by default, the current directory)
  93. -xR Remove a directory and its subdirectories from the database (by default, the current directory)" |
  94. fold -s -w $COLUMNS >&2
  95. }
  96. # Load zsh/datetime module, if necessary
  97. (( ${+EPOCHSECONDS} )) || zmodload zsh/datetime
  98. # Global associative array for internal use
  99. typeset -gA ZSHZ
  100. # Fallback utilities in case Zsh lacks zsh/files (as is the case with MobaXterm)
  101. ZSHZ[CHOWN]='chown'
  102. ZSHZ[MV]='mv'
  103. ZSHZ[RM]='rm'
  104. # Try to load zsh/files utilities
  105. if [[ ${builtins[zf_chown]-} != 'defined' ||
  106. ${builtins[zf_mv]-} != 'defined' ||
  107. ${builtins[zf_rm]-} != 'defined' ]]; then
  108. zmodload -F zsh/files b:zf_chown b:zf_mv b:zf_rm &> /dev/null
  109. fi
  110. # Use zsh/files, if it is available
  111. [[ ${builtins[zf_chown]-} == 'defined' ]] && ZSHZ[CHOWN]='zf_chown'
  112. [[ ${builtins[zf_mv]-} == 'defined' ]] && ZSHZ[MV]='zf_mv'
  113. [[ ${builtins[zf_rm]-} == 'defined' ]] && ZSHZ[RM]='zf_rm'
  114. # Load zsh/system, if necessary
  115. [[ ${modules[zsh/system]-} == 'loaded' ]] || zmodload zsh/system &> /dev/null
  116. # Make sure ZSHZ_EXCLUDE_DIRS has been declared so that other scripts can
  117. # simply append to it
  118. (( ${+ZSHZ_EXCLUDE_DIRS} )) || typeset -gUa ZSHZ_EXCLUDE_DIRS
  119. # Determine if zsystem flock is available
  120. zsystem supports flock &> /dev/null && ZSHZ[USE_FLOCK]=1
  121. # Determine if `print -v' is supported
  122. is-at-least 5.3.0 && ZSHZ[PRINTV]=1
  123. ############################################################
  124. # The Zsh-z Command
  125. #
  126. # Globals:
  127. # ZSHZ
  128. # ZSHZ_CASE
  129. # ZSHZ_CD
  130. # ZSHZ_COMPLETION
  131. # ZSHZ_DATA
  132. # ZSHZ_DEBUG
  133. # ZSHZ_EXCLUDE_DIRS
  134. # ZSHZ_KEEP_DIRS
  135. # ZSHZ_MAX_SCORE
  136. # ZSHZ_OWNER
  137. #
  138. # Arguments:
  139. # $* Command options and arguments
  140. ############################################################
  141. zshz() {
  142. # Don't use `emulate -L zsh' - it breaks PUSHD_IGNORE_DUPS
  143. setopt LOCAL_OPTIONS NO_KSH_ARRAYS NO_SH_WORD_SPLIT EXTENDED_GLOB UNSET
  144. (( ZSHZ_DEBUG )) && setopt LOCAL_OPTIONS WARN_CREATE_GLOBAL
  145. local REPLY
  146. local -a lines
  147. # Allow the user to specify a custom datafile in $ZSHZ_DATA (or legacy $_Z_DATA)
  148. local custom_datafile="${ZSHZ_DATA:-$_Z_DATA}"
  149. # If a datafile was provided as a standalone file without a directory path
  150. # print a warning and exit
  151. if [[ -n ${custom_datafile} && ${custom_datafile} != */* ]]; then
  152. print "ERROR: You configured a custom Zsh-z datafile (${custom_datafile}), but have not specified its directory." >&2
  153. exit
  154. fi
  155. # If the user specified a datafile, use that or default to ~/.z
  156. # If the datafile is a symlink, it gets dereferenced
  157. local datafile=${${custom_datafile:-$HOME/.z}:A}
  158. # If the datafile is a directory, print a warning and exit
  159. if [[ -d $datafile ]]; then
  160. print "ERROR: Zsh-z's datafile (${datafile}) is a directory." >&2
  161. exit
  162. fi
  163. # Make sure that the datafile exists before attempting to read it or lock it
  164. # for writing
  165. [[ -f $datafile ]] || { mkdir -p "${datafile:h}" && touch "$datafile" }
  166. # Bail if we don't own the datafile and $ZSHZ_OWNER is not set
  167. [[ -z ${ZSHZ_OWNER:-${_Z_OWNER}} && -f $datafile && ! -O $datafile ]] &&
  168. return
  169. # Load the datafile into an array and parse it
  170. lines=( ${(f)"$(< $datafile)"} )
  171. # Discard entries that are incomplete or incorrectly formatted
  172. lines=( ${(M)lines:#/*\|[[:digit:]]##[.,]#[[:digit:]]#\|[[:digit:]]##} )
  173. ############################################################
  174. # Add a path to or remove one from the datafile
  175. #
  176. # Globals:
  177. # ZSHZ
  178. # ZSHZ_EXCLUDE_DIRS
  179. # ZSHZ_OWNER
  180. #
  181. # Arguments:
  182. # $1 Which action to perform (--add/--remove)
  183. # $2 The path to add
  184. ############################################################
  185. _zshz_add_or_remove_path() {
  186. local action=${1}
  187. shift
  188. if [[ $action == '--add' ]]; then
  189. # TODO: The following tasks are now handled by _agkozak_precmd. Dead code?
  190. # Don't add $HOME
  191. [[ $* == $HOME ]] && return
  192. # Don't track directory trees excluded in ZSHZ_EXCLUDE_DIRS
  193. local exclude
  194. for exclude in ${(@)ZSHZ_EXCLUDE_DIRS:-${(@)_Z_EXCLUDE_DIRS}}; do
  195. case $* in
  196. ${exclude}|${exclude}/*) return ;;
  197. esac
  198. done
  199. fi
  200. # A temporary file that gets copied over the datafile if all goes well
  201. local tempfile="${datafile}.${RANDOM}"
  202. # See https://github.com/rupa/z/pull/199/commits/ed6eeed9b70d27c1582e3dd050e72ebfe246341c
  203. if (( ZSHZ[USE_FLOCK] )); then
  204. local lockfd
  205. # Grab exclusive lock (released when function exits)
  206. zsystem flock -f lockfd "$datafile" 2> /dev/null || return
  207. fi
  208. integer tmpfd
  209. case $action in
  210. --add)
  211. exec {tmpfd}>|"$tempfile" # Open up tempfile for writing
  212. _zshz_update_datafile $tmpfd "$*"
  213. local ret=$?
  214. ;;
  215. --remove)
  216. local xdir # Directory to be removed
  217. if (( ${ZSHZ_NO_RESOLVE_SYMLINKS:-${_Z_NO_RESOLVE_SYMLINKS}} )); then
  218. [[ -d ${${*:-${PWD}}:a} ]] && xdir=${${*:-${PWD}}:a}
  219. else
  220. [[ -d ${${*:-${PWD}}:A} ]] && xdir=${${*:-${PWD}}:a}
  221. fi
  222. local -a lines_to_keep
  223. if (( ${+opts[-R]} )); then
  224. # Prompt user before deleting entire database
  225. if [[ $xdir == '/' ]] && ! read -q "?Delete entire Zsh-z database? "; then
  226. print && return 1
  227. fi
  228. # All of the lines that don't match the directory to be deleted
  229. lines_to_keep=( ${lines:#${xdir}\|*} )
  230. # Or its subdirectories
  231. lines_to_keep=( ${lines_to_keep:#${xdir%/}/**} )
  232. else
  233. # All of the lines that don't match the directory to be deleted
  234. lines_to_keep=( ${lines:#${xdir}\|*} )
  235. fi
  236. if [[ $lines != "$lines_to_keep" ]]; then
  237. lines=( $lines_to_keep )
  238. else
  239. return 1 # The $PWD isn't in the datafile
  240. fi
  241. exec {tmpfd}>|"$tempfile" # Open up tempfile for writing
  242. print -u $tmpfd -l -- $lines
  243. local ret=$?
  244. ;;
  245. esac
  246. if (( tmpfd != 0 )); then
  247. # Close tempfile
  248. exec {tmpfd}>&-
  249. fi
  250. if (( ret != 0 )); then
  251. # Avoid clobbering the datafile if the write to tempfile failed
  252. ${ZSHZ[RM]} -f "$tempfile"
  253. return $ret
  254. fi
  255. local owner
  256. owner=${ZSHZ_OWNER:-${_Z_OWNER}}
  257. if (( ZSHZ[USE_FLOCK] )); then
  258. ${ZSHZ[MV]} "$tempfile" "$datafile" 2> /dev/null || ${ZSHZ[RM]} -f "$tempfile"
  259. if [[ -n $owner ]]; then
  260. ${ZSHZ[CHOWN]} ${owner}:"$(id -ng ${owner})" "$datafile"
  261. fi
  262. else
  263. if [[ -n $owner ]]; then
  264. ${ZSHZ[CHOWN]} "${owner}":"$(id -ng "${owner}")" "$tempfile"
  265. fi
  266. ${ZSHZ[MV]} -f "$tempfile" "$datafile" 2> /dev/null ||
  267. ${ZSHZ[RM]} -f "$tempfile"
  268. fi
  269. # In order to make z -x work, we have to disable zsh-z's adding
  270. # to the database until the user changes directory and the
  271. # chpwd_functions are run
  272. if [[ $action == '--remove' ]]; then
  273. ZSHZ[DIRECTORY_REMOVED]=1
  274. fi
  275. }
  276. ############################################################
  277. # Read the current datafile contents, update them, "age" them
  278. # when the total rank gets high enough, and print the new
  279. # contents to STDOUT.
  280. #
  281. # Globals:
  282. # ZSHZ_KEEP_DIRS
  283. # ZSHZ_MAX_SCORE
  284. #
  285. # Arguments:
  286. # $1 File descriptor linked to tempfile
  287. # $2 Path to be added to datafile
  288. ############################################################
  289. _zshz_update_datafile() {
  290. integer fd=$1
  291. local -A rank time
  292. # Characters special to the shell (such as '[]') are quoted with backslashes
  293. # See https://github.com/rupa/z/issues/246
  294. local add_path=${(q)2}
  295. local -a existing_paths
  296. local now=$EPOCHSECONDS line dir
  297. local path_field rank_field time_field count x
  298. rank[$add_path]=1
  299. time[$add_path]=$now
  300. # Remove paths from database if they no longer exist
  301. for line in $lines; do
  302. if [[ ! -d ${line%%\|*} ]]; then
  303. for dir in ${(@)ZSHZ_KEEP_DIRS}; do
  304. if [[ ${line%%\|*} == ${dir}/* ||
  305. ${line%%\|*} == $dir ||
  306. $dir == '/' ]]; then
  307. existing_paths+=( $line )
  308. fi
  309. done
  310. else
  311. existing_paths+=( $line )
  312. fi
  313. done
  314. lines=( $existing_paths )
  315. for line in $lines; do
  316. path_field=${(q)line%%\|*}
  317. rank_field=${${line%\|*}#*\|}
  318. time_field=${line##*\|}
  319. # When a rank drops below 1, drop the path from the database
  320. (( rank_field < 1 )) && continue
  321. if [[ $path_field == $add_path ]]; then
  322. rank[$path_field]=$rank_field
  323. (( rank[$path_field]++ ))
  324. time[$path_field]=$now
  325. else
  326. rank[$path_field]=$rank_field
  327. time[$path_field]=$time_field
  328. fi
  329. (( count += rank_field ))
  330. done
  331. if (( count > ${ZSHZ_MAX_SCORE:-${_Z_MAX_SCORE:-9000}} )); then
  332. # Aging
  333. for x in ${(k)rank}; do
  334. print -u $fd -- "$x|$(( 0.99 * rank[$x] ))|${time[$x]}" || return 1
  335. done
  336. else
  337. for x in ${(k)rank}; do
  338. print -u $fd -- "$x|${rank[$x]}|${time[$x]}" || return 1
  339. done
  340. fi
  341. }
  342. ############################################################
  343. # The original tab completion method
  344. #
  345. # String processing is smartcase -- case-insensitive if the
  346. # search string is lowercase, case-sensitive if there are
  347. # any uppercase letters. Spaces in the search string are
  348. # treated as *'s in globbing. Read the contents of the
  349. # datafile and print matches to STDOUT.
  350. #
  351. # Arguments:
  352. # $1 The string to be completed
  353. ############################################################
  354. _zshz_legacy_complete() {
  355. local line path_field path_field_normalized
  356. # Replace spaces in the search string with asterisks for globbing
  357. 1=${1//[[:space:]]/*}
  358. for line in $lines; do
  359. path_field=${line%%\|*}
  360. path_field_normalized=$path_field
  361. if (( ZSHZ_TRAILING_SLASH )); then
  362. path_field_normalized=${path_field%/}/
  363. fi
  364. # If the search string is all lowercase, the search will be case-insensitive
  365. if [[ $1 == "${1:l}" && ${path_field_normalized:l} == *${~1}* ]]; then
  366. print -- $path_field
  367. # Otherwise, case-sensitive
  368. elif [[ $path_field_normalized == *${~1}* ]]; then
  369. print -- $path_field
  370. fi
  371. done
  372. # TODO: Search strings with spaces in them are currently treated case-
  373. # insensitively.
  374. }
  375. ############################################################
  376. # `print' or `printf' to REPLY
  377. #
  378. # Variable assignment through command substitution, of the
  379. # form
  380. #
  381. # foo=$( bar )
  382. #
  383. # requires forking a subshell; on Cygwin/MSYS2/WSL1 that can
  384. # be surprisingly slow. Zsh-z avoids doing that by printing
  385. # values to the variable REPLY. Since Zsh v5.3.0 that has
  386. # been possible with `print -v'; for earlier versions of the
  387. # shell, the values are placed on the editing buffer stack
  388. # and then `read' into REPLY.
  389. #
  390. # Globals:
  391. # ZSHZ
  392. #
  393. # Arguments:
  394. # Options and parameters for `print'
  395. ############################################################
  396. _zshz_printv() {
  397. # NOTE: For a long time, ZSH's `print -v' had a tendency
  398. # to mangle multibyte strings:
  399. #
  400. # https://www.zsh.org/mla/workers/2020/msg00307.html
  401. #
  402. # The bug was fixed in late 2020:
  403. #
  404. # https://github.com/zsh-users/zsh/commit/b6ba74cd4eaec2b6cb515748cf1b74a19133d4a4#diff-32bbef18e126b837c87b06f11bfc61fafdaa0ed99fcb009ec53f4767e246b129
  405. #
  406. # In order to support shells with the bug, we must use a form of `printf`,
  407. # which does not exhibit the undesired behavior. See
  408. #
  409. # https://www.zsh.org/mla/workers/2020/msg00308.html
  410. if (( ZSHZ[PRINTV] )); then
  411. builtin print -v REPLY -f %s $@
  412. else
  413. builtin print -z $@
  414. builtin read -rz REPLY
  415. fi
  416. }
  417. ############################################################
  418. # If matches share a common root, find it, and put it in
  419. # REPLY for _zshz_output to use.
  420. #
  421. # Arguments:
  422. # $1 Name of associative array of matches and ranks
  423. ############################################################
  424. _zshz_find_common_root() {
  425. local -a common_matches
  426. local x short
  427. common_matches=( ${(@Pk)1} )
  428. for x in ${(@)common_matches}; do
  429. if [[ -z $short ]] || (( $#x < $#short )) || [[ $x != ${short}/* ]]; then
  430. short=$x
  431. fi
  432. done
  433. [[ $short == '/' ]] && return
  434. for x in ${(@)common_matches}; do
  435. [[ $x != $short* ]] && return
  436. done
  437. _zshz_printv -- $short
  438. }
  439. ############################################################
  440. # Calculate a common root, if there is one. Then do one of
  441. # the following:
  442. #
  443. # 1) Print a list of completions in frecent order;
  444. # 2) List them (z -l) to STDOUT; or
  445. # 3) Put a common root or best match into REPLY
  446. #
  447. # Globals:
  448. # ZSHZ_UNCOMMON
  449. #
  450. # Arguments:
  451. # $1 Name of an associative array of matches and ranks
  452. # $2 The best match or best case-insensitive match
  453. # $3 Whether to produce a completion, a list, or a root or
  454. # match
  455. ############################################################
  456. _zshz_output() {
  457. local match_array=$1 match=$2 format=$3
  458. local common k x
  459. local -a descending_list output
  460. local -A output_matches
  461. output_matches=( ${(Pkv)match_array} )
  462. _zshz_find_common_root $match_array
  463. common=$REPLY
  464. case $format in
  465. completion)
  466. for k in ${(@k)output_matches}; do
  467. _zshz_printv -f "%.2f|%s" ${output_matches[$k]} $k
  468. descending_list+=( ${(f)REPLY} )
  469. REPLY=''
  470. done
  471. descending_list=( ${${(@On)descending_list}#*\|} )
  472. print -l $descending_list
  473. ;;
  474. list)
  475. local path_to_display
  476. for x in ${(k)output_matches}; do
  477. if (( ${output_matches[$x]} )); then
  478. path_to_display=$x
  479. (( ZSHZ_TILDE )) &&
  480. path_to_display=${path_to_display/#${HOME}/\~}
  481. _zshz_printv -f "%-10d %s\n" ${output_matches[$x]} $path_to_display
  482. output+=( ${(f)REPLY} )
  483. REPLY=''
  484. fi
  485. done
  486. if [[ -n $common ]]; then
  487. (( ZSHZ_TILDE )) && common=${common/#${HOME}/\~}
  488. (( $#output > 1 )) && printf "%-10s %s\n" 'common:' $common
  489. fi
  490. # -lt
  491. if (( $+opts[-t] )); then
  492. for x in ${(@On)output}; do
  493. print -- $x
  494. done
  495. # -lr
  496. elif (( $+opts[-r] )); then
  497. for x in ${(@on)output}; do
  498. print -- $x
  499. done
  500. # -l
  501. else
  502. for x in ${(@on)output}; do
  503. print $x
  504. done
  505. fi
  506. ;;
  507. *)
  508. if (( ! ZSHZ_UNCOMMON )) && [[ -n $common ]]; then
  509. _zshz_printv -- $common
  510. else
  511. _zshz_printv -- ${(P)match}
  512. fi
  513. ;;
  514. esac
  515. }
  516. ############################################################
  517. # Match a pattern by rank, time, or a combination of the
  518. # two, and output the results as completions, a list, or a
  519. # best match.
  520. #
  521. # Globals:
  522. # ZSHZ
  523. # ZSHZ_CASE
  524. # ZSHZ_KEEP_DIRS
  525. # ZSHZ_OWNER
  526. #
  527. # Arguments:
  528. # #1 Pattern to match
  529. # $2 Matching method (rank, time, or [default] frecency)
  530. # $3 Output format (completion, list, or [default] store
  531. # in REPLY
  532. ############################################################
  533. _zshz_find_matches() {
  534. setopt LOCAL_OPTIONS NO_EXTENDED_GLOB
  535. local fnd=$1 method=$2 format=$3
  536. local -a existing_paths
  537. local line dir path_field rank_field time_field rank dx escaped_path_field
  538. local -A matches imatches
  539. local best_match ibest_match hi_rank=-9999999999 ihi_rank=-9999999999
  540. # Remove paths from database if they no longer exist
  541. for line in $lines; do
  542. if [[ ! -d ${line%%\|*} ]]; then
  543. for dir in ${(@)ZSHZ_KEEP_DIRS}; do
  544. if [[ ${line%%\|*} == ${dir}/* ||
  545. ${line%%\|*} == $dir ||
  546. $dir == '/' ]]; then
  547. existing_paths+=( $line )
  548. fi
  549. done
  550. else
  551. existing_paths+=( $line )
  552. fi
  553. done
  554. lines=( $existing_paths )
  555. for line in $lines; do
  556. path_field=${line%%\|*}
  557. rank_field=${${line%\|*}#*\|}
  558. time_field=${line##*\|}
  559. case $method in
  560. rank) rank=$rank_field ;;
  561. time) (( rank = time_field - EPOCHSECONDS )) ;;
  562. *)
  563. # Frecency routine
  564. (( dx = EPOCHSECONDS - time_field ))
  565. rank=$(( 10000 * rank_field * (3.75/( (0.0001 * dx + 1) + 0.25)) ))
  566. ;;
  567. esac
  568. # Use spaces as wildcards
  569. local q=${fnd//[[:space:]]/\*}
  570. # If $ZSHZ_TRAILING_SLASH is set, use path_field with a trailing slash for matching.
  571. local path_field_normalized=$path_field
  572. if (( ZSHZ_TRAILING_SLASH )); then
  573. path_field_normalized=${path_field%/}/
  574. fi
  575. # If $ZSHZ_CASE is 'ignore', be case-insensitive.
  576. #
  577. # If it's 'smart', be case-insensitive unless the string to be matched
  578. # includes capital letters.
  579. #
  580. # Otherwise, the default behavior of Zsh-z is to match case-sensitively if
  581. # possible, then to fall back on a case-insensitive match if possible.
  582. if [[ $ZSHZ_CASE == 'smart' && ${1:l} == $1 &&
  583. ${path_field_normalized:l} == ${~q:l} ]]; then
  584. imatches[$path_field]=$rank
  585. elif [[ $ZSHZ_CASE != 'ignore' && $path_field_normalized == ${~q} ]]; then
  586. matches[$path_field]=$rank
  587. elif [[ $ZSHZ_CASE != 'smart' && ${path_field_normalized:l} == ${~q:l} ]]; then
  588. imatches[$path_field]=$rank
  589. fi
  590. # Escape characters that would cause "invalid subscript" errors
  591. # when accessing the associative array.
  592. escaped_path_field=${path_field//'\'/'\\'}
  593. escaped_path_field=${escaped_path_field//'`'/'\`'}
  594. escaped_path_field=${escaped_path_field//'('/'\('}
  595. escaped_path_field=${escaped_path_field//')'/'\)'}
  596. escaped_path_field=${escaped_path_field//'['/'\['}
  597. escaped_path_field=${escaped_path_field//']'/'\]'}
  598. if (( matches[$escaped_path_field] )) &&
  599. (( matches[$escaped_path_field] > hi_rank )); then
  600. best_match=$path_field
  601. hi_rank=${matches[$escaped_path_field]}
  602. elif (( imatches[$escaped_path_field] )) &&
  603. (( imatches[$escaped_path_field] > ihi_rank )); then
  604. ibest_match=$path_field
  605. ihi_rank=${imatches[$escaped_path_field]}
  606. ZSHZ[CASE_INSENSITIVE]=1
  607. fi
  608. done
  609. # Return 1 when there are no matches
  610. [[ -z $best_match && -z $ibest_match ]] && return 1
  611. if [[ -n $best_match ]]; then
  612. _zshz_output matches best_match $format
  613. elif [[ -n $ibest_match ]]; then
  614. _zshz_output imatches ibest_match $format
  615. fi
  616. }
  617. # THE MAIN ROUTINE
  618. local -A opts
  619. zparseopts -E -D -A opts -- \
  620. -add \
  621. -complete \
  622. c \
  623. e \
  624. h \
  625. -help \
  626. l \
  627. r \
  628. R \
  629. t \
  630. x
  631. if [[ $1 == '--' ]]; then
  632. shift
  633. elif [[ -n ${(M)@:#-*} && -z $compstate ]]; then
  634. print "Improper option(s) given."
  635. _zshz_usage
  636. return 1
  637. fi
  638. local opt output_format method='frecency' fnd prefix req
  639. for opt in ${(k)opts}; do
  640. case $opt in
  641. --add)
  642. [[ ! -d $* ]] && return 1
  643. local dir
  644. # Cygwin and MSYS2 have a hard time with relative paths expressed from /
  645. if [[ $OSTYPE == (cygwin|msys) && $PWD == '/' && $* != /* ]]; then
  646. set -- "/$*"
  647. fi
  648. if (( ${ZSHZ_NO_RESOLVE_SYMLINKS:-${_Z_NO_RESOLVE_SYMLINKS}} )); then
  649. dir=${*:a}
  650. else
  651. dir=${*:A}
  652. fi
  653. _zshz_add_or_remove_path --add "$dir"
  654. return
  655. ;;
  656. --complete)
  657. if [[ -s $datafile && ${ZSHZ_COMPLETION:-frecent} == 'legacy' ]]; then
  658. _zshz_legacy_complete "$1"
  659. return
  660. fi
  661. output_format='completion'
  662. ;;
  663. -c) [[ $* == ${PWD}/* || $PWD == '/' ]] || prefix="$PWD " ;;
  664. -h|--help)
  665. _zshz_usage
  666. return
  667. ;;
  668. -l) output_format='list' ;;
  669. -r) method='rank' ;;
  670. -t) method='time' ;;
  671. -x)
  672. # Cygwin and MSYS2 have a hard time with relative paths expressed from /
  673. if [[ $OSTYPE == (cygwin|msys) && $PWD == '/' && $* != /* ]]; then
  674. set -- "/$*"
  675. fi
  676. _zshz_add_or_remove_path --remove $*
  677. return
  678. ;;
  679. esac
  680. done
  681. req="$*"
  682. fnd="$prefix$*"
  683. [[ -n $fnd && $fnd != "$PWD " ]] || {
  684. [[ $output_format != 'completion' ]] && output_format='list'
  685. }
  686. #########################################################
  687. # Allow the user to specify directory-changing command
  688. # using $ZSHZ_CD (default: builtin cd).
  689. #
  690. # Globals:
  691. # ZSHZ_CD
  692. #
  693. # Arguments:
  694. # $* Path
  695. #########################################################
  696. zshz_cd() {
  697. setopt LOCAL_OPTIONS NO_WARN_CREATE_GLOBAL
  698. if [[ -z $ZSHZ_CD ]]; then
  699. builtin cd "$*"
  700. else
  701. ${=ZSHZ_CD} "$*"
  702. fi
  703. }
  704. #########################################################
  705. # If $ZSHZ_ECHO == 1, display paths as you jump to them.
  706. # If it is also the case that $ZSHZ_TILDE == 1, display
  707. # the home directory as a tilde.
  708. #########################################################
  709. _zshz_echo() {
  710. if (( ZSHZ_ECHO )); then
  711. if (( ZSHZ_TILDE )); then
  712. print ${PWD/#${HOME}/\~}
  713. else
  714. print $PWD
  715. fi
  716. fi
  717. }
  718. if [[ ${@: -1} == /* ]] && (( ! $+opts[-e] && ! $+opts[-l] )); then
  719. # cd if possible; echo the new path if $ZSHZ_ECHO == 1
  720. [[ -d ${@: -1} ]] && zshz_cd ${@: -1} && _zshz_echo && return
  721. fi
  722. # With option -c, make sure query string matches beginning of matches;
  723. # otherwise look for matches anywhere in paths
  724. # zpm-zsh/colors has a global $c, so we'll avoid math expressions here
  725. if [[ ! -z ${(tP)opts[-c]} ]]; then
  726. _zshz_find_matches "$fnd*" $method $output_format
  727. else
  728. _zshz_find_matches "*$fnd*" $method $output_format
  729. fi
  730. local ret2=$?
  731. local cd
  732. cd=$REPLY
  733. # New experimental "uncommon" behavior
  734. #
  735. # 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
  736. # is `foo', go to /foo/bar/foo
  737. if (( ZSHZ_UNCOMMON )) && [[ -n $cd ]]; then
  738. if [[ -n $cd ]]; then
  739. # In the search pattern, replace spaces with *
  740. local q=${fnd//[[:space:]]/\*}
  741. q=${q%/} # Trailing slash has to be removed
  742. # As long as the best match is not case-insensitive
  743. if (( ! ZSHZ[CASE_INSENSITIVE] )); then
  744. # Count the number of characters in $cd that $q matches
  745. local q_chars=$(( ${#cd} - ${#${cd//${~q}/}} ))
  746. # Try dropping directory elements from the right; stop when it affects
  747. # how many times the search pattern appears
  748. until (( ( ${#cd:h} - ${#${${cd:h}//${~q}/}} ) != q_chars )); do
  749. cd=${cd:h}
  750. done
  751. # If the best match is case-insensitive
  752. else
  753. local q_chars=$(( ${#cd} - ${#${${cd:l}//${~${q:l}}/}} ))
  754. until (( ( ${#cd:h} - ${#${${${cd:h}:l}//${~${q:l}}/}} ) != q_chars )); do
  755. cd=${cd:h}
  756. done
  757. fi
  758. ZSHZ[CASE_INSENSITIVE]=0
  759. fi
  760. fi
  761. if (( ret2 == 0 )) && [[ -n $cd ]]; then
  762. if (( $+opts[-e] )); then # echo
  763. (( ZSHZ_TILDE )) && cd=${cd/#${HOME}/\~}
  764. print -- "$cd"
  765. else
  766. # cd if possible; echo the new path if $ZSHZ_ECHO == 1
  767. [[ -d $cd ]] && zshz_cd "$cd" && _zshz_echo
  768. fi
  769. else
  770. # if $req is a valid path, cd to it; echo the new path if $ZSHZ_ECHO == 1
  771. if ! (( $+opts[-e] || $+opts[-l] )) && [[ -d $req ]]; then
  772. zshz_cd "$req" && _zshz_echo
  773. else
  774. return $ret2
  775. fi
  776. fi
  777. }
  778. alias ${ZSHZ_CMD:-${_Z_CMD:-z}}='zshz 2>&1'
  779. ############################################################
  780. # precmd - add path to datafile unless `z -x' has just been
  781. # run
  782. #
  783. # Globals:
  784. # ZSHZ
  785. ############################################################
  786. _zshz_precmd() {
  787. # Protect against `setopt NO_UNSET'
  788. setopt LOCAL_OPTIONS UNSET
  789. # Do not add PWD to datafile when in HOME directory, or
  790. # if `z -x' has just been run
  791. [[ $PWD == "$HOME" ]] || (( ZSHZ[DIRECTORY_REMOVED] )) && return
  792. # Don't track directory trees excluded in ZSHZ_EXCLUDE_DIRS
  793. local exclude
  794. for exclude in ${(@)ZSHZ_EXCLUDE_DIRS:-${(@)_Z_EXCLUDE_DIRS}}; do
  795. case $PWD in
  796. ${exclude}|${exclude}/*) return ;;
  797. esac
  798. done
  799. # It appears that forking a subshell is so slow in Windows that it is better
  800. # just to add the PWD to the datafile in the foreground
  801. if [[ $OSTYPE == (cygwin|msys) ]]; then
  802. zshz --add "$PWD"
  803. else
  804. (zshz --add "$PWD" &)
  805. fi
  806. # See https://github.com/rupa/z/pull/247/commits/081406117ea42ccb8d159f7630cfc7658db054b6
  807. : $RANDOM
  808. }
  809. ############################################################
  810. # chpwd
  811. #
  812. # When the $PWD is removed from the datafile with `z -x',
  813. # Zsh-z refrains from adding it again until the user has
  814. # left the directory.
  815. #
  816. # Globals:
  817. # ZSHZ
  818. ############################################################
  819. _zshz_chpwd() {
  820. ZSHZ[DIRECTORY_REMOVED]=0
  821. }
  822. autoload -Uz add-zsh-hook
  823. add-zsh-hook precmd _zshz_precmd
  824. add-zsh-hook chpwd _zshz_chpwd
  825. ############################################################
  826. # Completion
  827. ############################################################
  828. # Standardized $0 handling
  829. # https://zdharma-continuum.github.io/Zsh-100-Commits-Club/Zsh-Plugin-Standard.html
  830. 0="${${ZERO:-${0:#$ZSH_ARGZERO}}:-${(%):-%N}}"
  831. 0="${${(M)0:#/*}:-$PWD/$0}"
  832. (( ${fpath[(ie)${0:A:h}]} <= ${#fpath} )) || fpath=( "${0:A:h}" "${fpath[@]}" )
  833. ############################################################
  834. # zsh-z functions
  835. ############################################################
  836. ZSHZ[FUNCTIONS]='_zshz_usage
  837. _zshz_add_or_remove_path
  838. _zshz_update_datafile
  839. _zshz_legacy_complete
  840. _zshz_printv
  841. _zshz_find_common_root
  842. _zshz_output
  843. _zshz_find_matches
  844. zshz
  845. _zshz_precmd
  846. _zshz_chpwd
  847. _zshz'
  848. ############################################################
  849. # Enable WARN_NESTED_VAR for functions listed in
  850. # ZSHZ[FUNCTIONS]
  851. ############################################################
  852. (( ${+ZSHZ_DEBUG} )) && () {
  853. if is-at-least 5.4.0; then
  854. local x
  855. for x in ${=ZSHZ[FUNCTIONS]}; do
  856. functions -W $x
  857. done
  858. fi
  859. }
  860. ############################################################
  861. # Unload function
  862. #
  863. # See https://github.com/agkozak/Zsh-100-Commits-Club/blob/master/Zsh-Plugin-Standard.adoc#unload-fun
  864. #
  865. # Globals:
  866. # ZSHZ
  867. # ZSHZ_CMD
  868. ############################################################
  869. zsh-z_plugin_unload() {
  870. emulate -L zsh
  871. add-zsh-hook -D precmd _zshz_precmd
  872. add-zsh-hook -d chpwd _zshz_chpwd
  873. local x
  874. for x in ${=ZSHZ[FUNCTIONS]}; do
  875. (( ${+functions[$x]} )) && unfunction $x
  876. done
  877. unset ZSHZ
  878. fpath=( "${(@)fpath:#${0:A:h}}" )
  879. (( ${+aliases[${ZSHZ_CMD:-${_Z_CMD:-z}}]} )) &&
  880. unalias ${ZSHZ_CMD:-${_Z_CMD:-z}}
  881. unfunction $0
  882. }
  883. # vim: fdm=indent:ts=2:et:sts=2:sw=2: