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