z.plugin.zsh 29 KB

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