z.plugin.zsh 30 KB

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