Skip to main content

zsh/compsys/ported/
compinit.rs

1//! Port of `compinit` from `Completion/compinit`.
2//!
3//! Full upstream body (574 lines verbatim):
4//! ```text
5//! sh:  1  # Initialisation for new style completion. This mainly contains some helper
6//! sh:  2  # functions and setup. Everything else is split into different files that
7//! sh:  3  # will automatically be made autoloaded (see the end of this file).  The
8//! sh:  4  # names of the files that will be considered for autoloading are those that
9//! sh:  5  # begin with an underscores (like `_condition).
10//! sh:  6  #
11//! sh:  7  # The first line of each of these files is read and must indicate what
12//! sh:  8  # should be done with its contents:
13//! sh:  9  #
14//! sh: 10  #   `#compdef <names ...>'
15//! sh: 11  #     If the first line looks like this, the file is autoloaded as a
16//! sh: 12  #     function and that function will be called to generate the matches
17//! sh: 13  #     when completing for one of the commands whose <names> are given.
18//! sh: 14  #     The names may also be interspersed with `-T <assoc>' options
19//! sh: 15  #     specifying for which set of functions this should be added.
20//! sh: 16  #
21//! sh: 17  #   `#compdef -[pP] <patterns ...>'
22//! sh: 18  #     This defines a function that should be called to generate matches
23//! sh: 19  #     for commands whose name matches <pattern>. Note that only one pattern
24//! sh: 20  #     may be given.
25//! sh: 21  #
26//! sh: 22  #   `#compdef -k <style> [ <key-sequence> ... ]'
27//! sh: 23  #     This is used to bind special completions to all the given
28//! sh: 24  #     <key-sequence>(s). The <style> is the name of one of the built-in
29//! sh: 25  #     completion widgets (complete-word, delete-char-or-list,
30//! sh: 26  #     expand-or-complete, expand-or-complete-prefix, list-choices,
31//! sh: 27  #     menu-complete, menu-expand-or-complete, or reverse-menu-complete).
32//! sh: 28  #     This creates a widget behaving like <style> so that the
33//! sh: 29  #     completions are chosen as given in the rest of the file,
34//! sh: 30  #     rather than by the context.  The widget has the same name as
35//! sh: 31  #     the autoload file and can be bound using bindkey in the normal way.
36//! sh: 32  #
37//! sh: 33  #   `#compdef -K <widget-name> <style> <key-sequence> [ ... ]'
38//! sh: 34  #     This is similar to -k, except it takes any number of sets of
39//! sh: 35  #     three arguments.  In each set, the widget <widget-name> will
40//! sh: 36  #     be defined, which will behave as <style>, as with -k, and will
41//! sh: 37  #     be bound to <key-sequence>, exactly one of which must be defined.
42//! sh: 38  #     <widget-name> must be different for each:  this must begin with an
43//! sh: 39  #     underscore, else one will be added, and should not clash with other
44//! sh: 40  #     completion widgets (names based on the name of the function are the
45//! sh: 41  #     clearest), but is otherwise arbitrary.  It can be tested in the
46//! sh: 42  #     function by the parameter $WIDGET.
47//! sh: 43  #
48//! sh: 44  #   `#autoload [ <options> ]'
49//! sh: 45  #     This is for helper functions that are not used to
50//! sh: 46  #     generate matches, but should automatically be loaded
51//! sh: 47  #     when they are called. The <options> will be given to the
52//! sh: 48  #     autoload builtin when making the function autoloaded. Note
53//! sh: 49  #     that this need not include `-U' and `-z'.
54//! sh: 50  #
55//! sh: 51  # Note that no white space is allowed between the `#' and the rest of
56//! sh: 52  # the string.
57//! sh: 53  #
58//! sh: 54  # Functions that are used to generate matches should return zero if they
59//! sh: 55  # were able to add matches and non-zero otherwise.
60//! sh: 56  #
61//! sh: 57  # See the file `compdump' for how to speed up initialisation.
62//! sh: 58
63//! sh: 59  # If we got the `-d'-flag, we will automatically dump the new state (at
64//! sh: 60  # the end).  This takes the dumpfile as an argument.  -d (with the
65//! sh: 61  # default dumpfile) is now the default; to turn off dumping use -D.
66//! sh: 62
67//! sh: 63  # If the dumpfile is being regenerated and you don't know why, you can use
68//! sh: 64  # the -w flag to see if it was because -D was passed, zsh version mismatched,
69//! sh: 65  # or number of files in $fpath differed.
70//! sh: 66
71//! sh: 67  # The -C flag bypasses both the check for rebuilding the dump file and the
72//! sh: 68  # usual call to compaudit; the -i flag causes insecure directories found by
73//! sh: 69  # compaudit to be ignored, and the -u flag causes all directories found by
74//! sh: 70  # compaudit to be used (without security checking).  Otherwise the user is
75//! sh: 71  # queried for whether to use or ignore the insecure directories (which
76//! sh: 72  # means compinit should not be called from non-interactive shells).
77//! sh: 73
78//! sh: 74  emulate -L zsh
79//! sh: 75  setopt extendedglob
80//! sh: 76
81//! sh: 77  typeset _i_dumpfile _i_files _i_line _i_done _i_dir _i_autodump=1
82//! sh: 78  typeset _i_tag _i_file _i_addfiles _i_fail=ask _i_check=yes _i_name _i_why
83//! sh: 79
84//! sh: 80  while [[ $# -gt 0 && $1 = -[dDiuCw] ]]; do
85//! sh: 81    case "$1" in
86//! sh: 82    -d)
87//! sh: 83      _i_autodump=1
88//! sh: 84      shift
89//! sh: 85      if [[ $# -gt 0 && "$1" != -[dfQC] ]]; then
90//! sh: 86        _i_dumpfile="$1"
91//! sh: 87        shift
92//! sh: 88      fi
93//! sh: 89      ;;
94//! sh: 90    -D)
95//! sh: 91      _i_autodump=0
96//! sh: 92      shift
97//! sh: 93      ;;
98//! sh: 94    -i)
99//! sh: 95      _i_fail=ign
100//! sh: 96      shift
101//! sh: 97      ;;
102//! sh: 98    -u)
103//! sh: 99      _i_fail=use
104//! sh:100      shift
105//! sh:101      ;;
106//! sh:102    -C)
107//! sh:103      _i_check=
108//! sh:104      shift
109//! sh:105      ;;
110//! sh:106    -w)
111//! sh:107      _i_why=1
112//! sh:108      shift
113//! sh:109      ;;
114//! sh:110    esac
115//! sh:111  done
116//! sh:112
117//! sh:113  # The associative arrays containing the definitions for the commands and
118//! sh:114  # services.
119//! sh:115
120//! sh:116  typeset -gHA _comps _services _patcomps _postpatcomps
121//! sh:117
122//! sh:118  # `_compautos' contains the names and options for autoloaded functions
123//! sh:119  # that get options.
124//! sh:120
125//! sh:121  typeset -gHA _compautos
126//! sh:122
127//! sh:123  # The associative array use to report information about the last
128//! sh:124  # completion to the outside.
129//! sh:125
130//! sh:126  typeset -gHA _lastcomp
131//! sh:127
132//! sh:128  # Remember dumpfile.
133//! sh:129  if [[ -n $_i_dumpfile ]]; then
134//! sh:130    # Explicitly supplied dumpfile.
135//! sh:131    typeset -g _comp_dumpfile="$_i_dumpfile"
136//! sh:132  else
137//! sh:133    typeset -g _comp_dumpfile="${ZDOTDIR:-$HOME}/.zcompdump"
138//! sh:134  fi
139//! sh:135
140//! sh:136  # The standard options set in completion functions.
141//! sh:137
142//! sh:138  typeset -gHa _comp_options
143//! sh:139  _comp_options=(
144//! sh:140         bareglobqual
145//! sh:141         extendedglob
146//! sh:142         glob
147//! sh:143         multibyte
148//! sh:144         multifuncdef
149//! sh:145         nullglob
150//! sh:146         rcexpandparam
151//! sh:147         unset
152//! sh:148      NO_allexport
153//! sh:149      NO_aliases
154//! sh:150      NO_autonamedirs
155//! sh:151      NO_cshnullglob
156//! sh:152      NO_cshjunkiequotes
157//! sh:153      NO_errexit
158//! sh:154      NO_errreturn
159//! sh:155      NO_globassign
160//! sh:156      NO_globsubst
161//! sh:157      NO_histsubstpattern
162//! sh:158      NO_ignorebraces
163//! sh:159      NO_ignoreclosebraces
164//! sh:160      NO_kshglob
165//! sh:161      NO_ksharrays
166//! sh:162      NO_kshtypeset
167//! sh:163      NO_markdirs
168//! sh:164      NO_octalzeroes
169//! sh:165      NO_posixbuiltins
170//! sh:166      NO_posixidentifiers
171//! sh:167      NO_shwordsplit
172//! sh:168      NO_shglob
173//! sh:169      NO_typesettounset
174//! sh:170      NO_warnnestedvar
175//! sh:171      NO_warncreateglobal
176//! sh:172  )
177//! sh:173
178//! sh:174  # And this one should be `eval'ed at the beginning of every entry point
179//! sh:175  # to the completion system.  It sets up what we currently consider a
180//! sh:176  # sane environment.  That means we set the options above, make sure we
181//! sh:177  # have a valid stdin descriptor (zle closes it before calling widgets)
182//! sh:178  # and don't get confused by user's ZERR trap handlers.
183//! sh:179
184//! sh:180  typeset -gH _comp_setup='local -A _comp_caller_options;
185//! sh:181               _comp_caller_options=(${(kv)options[@]});
186//! sh:182               setopt localoptions localtraps localpatterns ${_comp_options[@]};
187//! sh:183               local IFS=$'\'\ \\t\\r\\n\\0\'';
188//! sh:184               builtin enable -p \| \~ \( \? \* \[ \< \^ \# 2>&-;
189//! sh:185               exec </dev/null;
190//! sh:186               trap - ZERR;
191//! sh:187               local -a reply;
192//! sh:188               local REPLY;
193//! sh:189               local REPORTTIME;
194//! sh:190               unset REPORTTIME'
195//! sh:191
196//! sh:192  # These can hold names of functions that are to be called before/after all
197//! sh:193  # matches have been generated.
198//! sh:194
199//! sh:195  typeset -ga compprefuncs comppostfuncs
200//! sh:196  compprefuncs=()
201//! sh:197  comppostfuncs=()
202//! sh:198
203//! sh:199  # Loading it now ensures that the `funcstack' parameter is always correct.
204//! sh:200
205//! sh:201  : $funcstack
206//! sh:202
207//! sh:203  # This function is used to register or delete completion functions. For
208//! sh:204  # registering completion functions, it is invoked with the name of the
209//! sh:205  # function as it's first argument (after the options). The other
210//! sh:206  # arguments depend on what type of completion function is defined. If
211//! sh:207  # none of the `-p' and `-k' options is given a function for a command is
212//! sh:208  # defined. The arguments after the function name are then interpreted as
213//! sh:209  # the names of the command for which the function generates matches.
214//! sh:210  # With the `-p' option a function for a name pattern is defined. This
215//! sh:211  # function will be invoked when completing for a command whose name
216//! sh:212  # matches the pattern given as argument after the function name (in this
217//! sh:213  # case only one argument is accepted).
218//! sh:214  # The option `-P' is like `-p', but the function will be called after
219//! sh:215  # trying to find a function defined for the command on the line if no
220//! sh:216  # such function could be found.
221//! sh:217  # With the `-k' option a function for a special completion keys is
222//! sh:218  # defined and immediately bound to those keys. Here, the extra arguments
223//! sh:219  # are the name of one of the builtin completion widgets and any number
224//! sh:220  # of key specifications as accepted by the `bindkey' builtin.
225//! sh:221  # In any case the `-a' option may be given which makes the function
226//! sh:222  # whose name is given as the first argument be autoloaded. When defining
227//! sh:223  # a function for command names the `-n' option may be given and keeps
228//! sh:224  # the definitions from overriding any previous definitions for the
229//! sh:225  # commands; with `-k', the `-n' option prevents compdef from rebinding
230//! sh:226  # a key sequence which is already bound.
231//! sh:227  # For deleting definitions, the `-d' option must be given. Without the
232//! sh:228  # `-p' option, this deletes definitions for functions for the commands
233//! sh:229  # whose names are given as arguments. If combined with the `-p' option
234//! sh:230  # it deletes the definitions for the patterns given as argument.
235//! sh:231  # The `-d' option may not be combined with the `-k' option, i.e.
236//! sh:232  # definitions for key function can not be removed.
237//! sh:233  #
238//! sh:234  # Examples:
239//! sh:235  #
240//! sh:236  #  compdef -a foo bar baz
241//! sh:237  #    make the completion for the commands `bar' and `baz' use the
242//! sh:238  #    function `foo' and make this function be autoloaded
243//! sh:239  #
244//! sh:240  #  compdef -p foo 'c*'
245//! sh:241  #    make completion for all command whose name begins with a `c'
246//! sh:242  #    generate matches by calling the function `foo' before generating
247//! sh:243  #    matches defined for the command itself
248//! sh:244  #
249//! sh:245  #  compdef -k foo list-choices '^X^M' '\C-xm'
250//! sh:246  #    make the function `foo' be invoked when typing `Control-X Control-M'
251//! sh:247  #    or `Control-X m'; the function should generate matches and will
252//! sh:248  #    behave like the `list-choices' builtin widget
253//! sh:249  #
254//! sh:250  #  compdef -d bar baz
255//! sh:251  #   delete the definitions for the command names `bar' and `baz'
256//! sh:252
257//! sh:253  compdef() {
258//! sh:254    local opt autol type func delete eval new i ret=0 cmd svc
259//! sh:255    local -a match mbegin mend
260//! sh:256
261//! sh:257    emulate -L zsh
262//! sh:258    setopt extendedglob
263//! sh:259
264//! sh:260    # Get the options.
265//! sh:261
266//! sh:262    if (( ! $# )); then
267//! sh:263      print -u2 "$0: I need arguments"
268//! sh:264      return 1
269//! sh:265    fi
270//! sh:266
271//! sh:267    while getopts "anpPkKde" opt; do
272//! sh:268      case "$opt" in
273//! sh:269      a)    autol=yes;;
274//! sh:270      n)    new=yes;;
275//! sh:271      [pPkK]) if [[ -n "$type" ]]; then
276//! sh:272              # Error if both `-p' and `-k' are given (or one of them
277//! sh:273  	    # twice).
278//! sh:274              print -u2 "$0: type already set to $type"
279//! sh:275  	    return 1
280//! sh:276  	  fi
281//! sh:277  	  if [[ "$opt" = p ]]; then
282//! sh:278  	    type=pattern
283//! sh:279  	  elif [[ "$opt" = P ]]; then
284//! sh:280  	    type=postpattern
285//! sh:281  	  elif [[ "$opt" = K ]]; then
286//! sh:282  	    type=widgetkey
287//! sh:283  	  else
288//! sh:284  	    type=key
289//! sh:285  	  fi
290//! sh:286  	  ;;
291//! sh:287      d) delete=yes;;
292//! sh:288      e) eval=yes;;
293//! sh:289      esac
294//! sh:290    done
295//! sh:291    shift OPTIND-1
296//! sh:292
297//! sh:293    if (( ! $# )); then
298//! sh:294      print -u2 "$0: I need arguments"
299//! sh:295      return 1
300//! sh:296    fi
301//! sh:297
302//! sh:298    if [[ -z "$delete" ]]; then
303//! sh:299      # If the first word contains an equal sign, all words must contain one
304//! sh:300      # and we define which services to use for the commands.
305//! sh:301
306//! sh:302      if [[ -z "$eval" ]] && [[ "$1" = *\=* ]]; then
307//! sh:303        while (( $# )); do
308//! sh:304          if [[ "$1" = *\=* ]]; then
309//! sh:305  	  cmd="${1%%\=*}"
310//! sh:306  	  svc="${1#*\=}"
311//! sh:307            func="$_comps[${_services[(r)$svc]:-$svc}]"
312//! sh:308            [[ -n ${_services[$svc]} ]] &&
313//! sh:309                svc=${_services[$svc]}
314//! sh:310  	  [[ -z "$func" ]] &&
315//! sh:311  	      func="${${_patcomps[(K)$svc][1]}:-${_postpatcomps[(K)$svc][1]}}"
316//! sh:312            if [[ -n "$func" ]]; then
317//! sh:313  	    _comps[$cmd]="$func"
318//! sh:314  	    _services[$cmd]="$svc"
319//! sh:315  	  else
320//! sh:316  	    print -u2 "$0: unknown command or service: $svc"
321//! sh:317  	    ret=1
322//! sh:318  	  fi
323//! sh:319  	else
324//! sh:320  	  print -u2 "$0: invalid argument: $1"
325//! sh:321  	  ret=1
326//! sh:322  	fi
327//! sh:323          shift
328//! sh:324        done
329//! sh:325
330//! sh:326        return ret
331//! sh:327      fi
332//! sh:328
333//! sh:329      # Adding definitions, first get the name of the function name
334//! sh:330      # and probably do autoloading.
335//! sh:331
336//! sh:332      func="$1"
337//! sh:333      [[ -n "$autol" ]] && autoload -rUz "$func"
338//! sh:334      shift
339//! sh:335
340//! sh:336      case "$type" in
341//! sh:337      widgetkey)
342//! sh:338        while [[ -n $1 ]]; do
343//! sh:339  	if [[ $# -lt 3 ]]; then
344//! sh:340  	  print -u2 "$0: compdef -K requires <widget> <comp-widget> <key>"
345//! sh:341  	  return 1
346//! sh:342  	fi
347//! sh:343  	[[ $1 = _* ]] || 1="_$1"
348//! sh:344  	[[ $2 = .* ]] || 2=".$2"
349//! sh:345          [[ $2 = .menu-select ]] && zmodload -i zsh/complist
350//! sh:346  	zle -C "$1" "$2" "$func"
351//! sh:347  	if [[ -n $new ]]; then
352//! sh:348  	  bindkey "$3" | IFS=$' \t' read -A opt
353//! sh:349  	  [[ $opt[-1] = undefined-key ]] && bindkey "$3" "$1"
354//! sh:350  	else
355//! sh:351  	  bindkey "$3" "$1"
356//! sh:352  	fi
357//! sh:353  	shift 3
358//! sh:354        done
359//! sh:355        ;;
360//! sh:356      key)
361//! sh:357        if [[ $# -lt 2 ]]; then
362//! sh:358          print -u2 "$0: missing keys"
363//! sh:359  	return 1
364//! sh:360        fi
365//! sh:361
366//! sh:362        # Define the widget.
367//! sh:363        if [[ $1 = .* ]]; then
368//! sh:364          [[ $1 = .menu-select ]] && zmodload -i zsh/complist
369//! sh:365  	zle -C "$func" "$1" "$func"
370//! sh:366        else
371//! sh:367          [[ $1 = menu-select ]] && zmodload -i zsh/complist
372//! sh:368  	zle -C "$func" ".$1" "$func"
373//! sh:369        fi
374//! sh:370        shift
375//! sh:371
376//! sh:372        # And bind the keys...
377//! sh:373        for i; do
378//! sh:374          if [[ -n $new ]]; then
379//! sh:375  	   bindkey "$i" | IFS=$' \t' read -A opt
380//! sh:376  	   [[ $opt[-1] = undefined-key ]] || continue
381//! sh:377  	fi
382//! sh:378          bindkey "$i" "$func"
383//! sh:379        done
384//! sh:380        ;;
385//! sh:381      *)
386//! sh:382        # For commands store the function name in the
387//! sh:383        # associative array, command names as keys.
388//! sh:384        while (( $# )); do
389//! sh:385          if [[ "$1" = -N ]]; then
390//! sh:386            type=normal
391//! sh:387          elif [[ "$1" = -p ]]; then
392//! sh:388            type=pattern
393//! sh:389          elif [[ "$1" = -P ]]; then
394//! sh:390            type=postpattern
395//! sh:391          else
396//! sh:392            case "$type" in
397//! sh:393            pattern)
398//! sh:394  	    if [[ $1 = (#b)(*)=(*) ]]; then
399//! sh:395  	      _patcomps[$match[1]]="=$match[2]=$func"
400//! sh:396  	    else
401//! sh:397  	      _patcomps[$1]="$func"
402//! sh:398  	    fi
403//! sh:399              ;;
404//! sh:400            postpattern)
405//! sh:401  	    if [[ $1 = (#b)(*)=(*) ]]; then
406//! sh:402  	      _postpatcomps[$match[1]]="=$match[2]=$func"
407//! sh:403  	    else
408//! sh:404  	      _postpatcomps[$1]="$func"
409//! sh:405  	    fi
410//! sh:406              ;;
411//! sh:407            *)
412//! sh:408              if [[ "$1" = *\=* ]]; then
413//! sh:409  	      cmd="${1%%\=*}"
414//! sh:410  	      svc=yes
415//! sh:411              else
416//! sh:412  	      cmd="$1"
417//! sh:413  	      svc=
418//! sh:414              fi
419//! sh:415              if [[ -z "$new" || -z "${_comps[$1]}" ]]; then
420//! sh:416                _comps[$cmd]="$func"
421//! sh:417  	      [[ -n "$svc" ]] && _services[$cmd]="${1#*\=}"
422//! sh:418  	    fi
423//! sh:419              ;;
424//! sh:420            esac
425//! sh:421          fi
426//! sh:422          shift
427//! sh:423        done
428//! sh:424        ;;
429//! sh:425      esac
430//! sh:426    else
431//! sh:427      # Handle the `-d' option, deleting.
432//! sh:428
433//! sh:429      case "$type" in
434//! sh:430      pattern)
435//! sh:431        unset "_patcomps[$^@]"
436//! sh:432        ;;
437//! sh:433      postpattern)
438//! sh:434        unset "_postpatcomps[$^@]"
439//! sh:435        ;;
440//! sh:436      key)
441//! sh:437        # Oops, cannot do that yet.
442//! sh:438
443//! sh:439        print -u2 "$0: cannot restore key bindings"
444//! sh:440        return 1
445//! sh:441        ;;
446//! sh:442      *)
447//! sh:443        unset "_comps[$^@]"
448//! sh:444      esac
449//! sh:445    fi
450//! sh:446  }
451//! sh:447
452//! sh:448  # Now we automatically make the definition files autoloaded.
453//! sh:449
454//! sh:450  typeset _i_wdirs _i_wfiles
455//! sh:451
456//! sh:452  _i_wdirs=()
457//! sh:453  _i_wfiles=()
458//! sh:454
459//! sh:455  autoload -RUz compaudit
460//! sh:456  if [[ -n "$_i_check" ]]; then
461//! sh:457    typeset _i_q
462//! sh:458    if ! eval compaudit; then
463//! sh:459      if [[ -n "$_i_q" ]]; then
464//! sh:460        if [[ "$_i_fail" = ask ]]; then
465//! sh:461          if ! read -q \
466//! sh:462  "?zsh compinit: insecure $_i_q, run compaudit for list.
467//! sh:463  Ignore insecure $_i_q and continue [y] or abort compinit [n]? "; then
468//! sh:464  	  print -u2 "$0: initialization aborted"
469//! sh:465            unfunction compinit compdef
470//! sh:466            unset _comp_dumpfile _comp_secure compprefuncs comppostfuncs \
471//! sh:467                  _comps _patcomps _postpatcomps _compautos _lastcomp
472//! sh:468
473//! sh:469            return 1
474//! sh:470          fi
475//! sh:471        fi
476//! sh:472        fpath=(${fpath:|_i_wdirs})
477//! sh:473        (( $#_i_wfiles )) && _i_files=( "${(@)_i_files:#(${(j:|:)_i_wfiles%.zwc})}"  )
478//! sh:474        (( $#_i_wdirs ))  && _i_files=( "${(@)_i_files:#(${(j:|:)_i_wdirs%.zwc})/*}" )
479//! sh:475      fi
480//! sh:476      typeset -g _comp_secure=yes
481//! sh:477    fi
482//! sh:478  fi
483//! sh:479
484//! sh:480  # Make sure compdump is available, even if we aren't going to use it.
485//! sh:481  autoload -RUz compdump compinstall
486//! sh:482
487//! sh:483  # If we have a dump file, load it.
488//! sh:484
489//! sh:485  _i_done=''
490//! sh:486
491//! sh:487  if [[ -f "$_comp_dumpfile" ]]; then
492//! sh:488    if [[ -n "$_i_check" ]]; then
493//! sh:489      IFS=$' \t' read -rA _i_line < "$_comp_dumpfile"
494//! sh:490      if [[ _i_autodump -eq 1 && $_i_line[2] -eq $#_i_files &&
495//! sh:491          $ZSH_VERSION = $_i_line[4] ]]
496//! sh:492      then
497//! sh:493        builtin . "$_comp_dumpfile"
498//! sh:494        _i_done=yes
499//! sh:495      elif [[ _i_why -eq 1 ]]; then
500//! sh:496        print -nu2 "Loading dump file skipped, regenerating"
501//! sh:497        local pre=" because: "
502//! sh:498        if [[ _i_autodump -ne 1 ]]; then
503//! sh:499          print -nu2 $pre"-D flag given"
504//! sh:500          pre=", "
505//! sh:501        fi
506//! sh:502        if [[ $_i_line[2] -ne $#_i_files ]]; then
507//! sh:503          print -nu2 $pre"number of files in dump $_i_line[2] differ from files found in \$fpath $#_i_files"
508//! sh:504          pre=", "
509//! sh:505        fi
510//! sh:506        if [[ $ZSH_VERSION != $_i_line[4] ]]; then
511//! sh:507          print -nu2 $pre"zsh version changed from $_i_line[4] to $ZSH_VERSION"
512//! sh:508        fi
513//! sh:509        print -u2
514//! sh:510      fi
515//! sh:511    else
516//! sh:512      builtin . "$_comp_dumpfile"
517//! sh:513      _i_done=yes
518//! sh:514    fi
519//! sh:515  elif [[ _i_why -eq 1 ]]; then
520//! sh:516    print -u2 "No existing compdump file found, regenerating"
521//! sh:517  fi
522//! sh:518  if [[ -z "$_i_done" ]]; then
523//! sh:519    typeset -A _i_test
524//! sh:520
525//! sh:521    for _i_dir in $fpath; do
526//! sh:522      [[ $_i_dir = . ]] && continue
527//! sh:523      (( $_i_wdirs[(I)$_i_dir] )) && continue
528//! sh:524      for _i_file in $_i_dir/^([^_]*|*[\;\|\&]*|*~|*.zwc)(N); do
529//! sh:525        _i_name="${_i_file:t}"
530//! sh:526        (( $+_i_test[$_i_name] + $_i_wfiles[(I)$_i_file] )) && continue
531//! sh:527        _i_test[$_i_name]=yes
532//! sh:528        IFS=$' \t' read -rA _i_line < $_i_file
533//! sh:529        _i_tag=$_i_line[1]
534//! sh:530        shift _i_line
535//! sh:531        case $_i_tag in
536//! sh:532        (\#compdef)
537//! sh:533  	if [[ $_i_line[1] = -[pPkK](n|) ]]; then
538//! sh:534  	  compdef ${_i_line[1]}na "${_i_name}" "${(@)_i_line[2,-1]}"
539//! sh:535  	else
540//! sh:536  	  compdef -na "${_i_name}" "${_i_line[@]}"
541//! sh:537  	fi
542//! sh:538  	;;
543//! sh:539        (\#autoload)
544//! sh:540  	autoload -rUz "$_i_line[@]" ${_i_name}
545//! sh:541  	[[ "$_i_line" != \ # ]] && _compautos[${_i_name}]="$_i_line"
546//! sh:542  	;;
547//! sh:543        esac
548//! sh:544      done
549//! sh:545    done
550//! sh:546
551//! sh:547    # If autodumping was requested, do it now.
552//! sh:548
553//! sh:549    if [[ $_i_autodump = 1 ]]; then
554//! sh:550      compdump
555//! sh:551    fi
556//! sh:552  fi
557//! sh:553
558//! sh:554  # Rebind the standard widgets
559//! sh:555  for _i_line in complete-word delete-char-or-list expand-or-complete \
560//! sh:556    expand-or-complete-prefix list-choices menu-complete \
561//! sh:557    menu-expand-or-complete reverse-menu-complete; do
562//! sh:558    zle -C $_i_line .$_i_line _main_complete
563//! sh:559  done
564//! sh:560  zle -la menu-select && zle -C menu-select .menu-select _main_complete
565//! sh:561
566//! sh:562  # If the default completer set includes _expand, and tab is bound
567//! sh:563  # to expand-or-complete, rebind it to complete-word instead.
568//! sh:564  bindkey '^i' | IFS=$' \t' read -A _i_line
569//! sh:565  if [[ ${_i_line[2]} = expand-or-complete ]] &&
570//! sh:566    zstyle -a ':completion:' completer _i_line &&
571//! sh:567    (( ${_i_line[(i)_expand]} <= ${#_i_line} )); then
572//! sh:568    bindkey '^i' complete-word
573//! sh:569  fi
574//! sh:570
575//! sh:571  unfunction compinit compaudit
576//! sh:572  autoload -RUz compinit compaudit
577//! sh:573
578//! sh:574  return 0
579//! ```
580
581use rayon::prelude::*;
582use std::collections::{HashMap, HashSet};
583use std::fs;
584use std::path::{Path, PathBuf};
585use std::sync::Mutex;
586use std::time::Instant;
587
588// =====================================================================
589// Upstream constants (sh:138-197) — exported so every compsys entry
590// point can replay the `_comp_setup` eval and the `_comp_options`
591// list that compinit installs at load time.
592// =====================================================================
593
594/// sh:139-172 — the 33 options compinit forces into every compsys
595/// entry-point scope via `setopt localoptions … ${_comp_options[@]}`.
596/// Mirrors the upstream array verbatim, including the `NO_` prefix
597/// form for negated flags. Consumed by the eval string at
598/// [`COMP_SETUP_EVAL`].
599pub const COMP_OPTIONS: &[&str] = &[
600    "bareglobqual",
601    "extendedglob",
602    "glob",
603    "multibyte",
604    "multifuncdef",
605    "nullglob",
606    "rcexpandparam",
607    "unset",
608    "NO_allexport",
609    "NO_aliases",
610    "NO_autonamedirs",
611    "NO_cshnullglob",
612    "NO_cshjunkiequotes",
613    "NO_errexit",
614    "NO_errreturn",
615    "NO_globassign",
616    "NO_globsubst",
617    "NO_histsubstpattern",
618    "NO_ignorebraces",
619    "NO_ignoreclosebraces",
620    "NO_kshglob",
621    "NO_ksharrays",
622    "NO_kshtypeset",
623    "NO_markdirs",
624    "NO_octalzeroes",
625    "NO_posixbuiltins",
626    "NO_posixidentifiers",
627    "NO_shwordsplit",
628    "NO_shglob",
629    "NO_typesettounset",
630    "NO_warnnestedvar",
631    "NO_warncreateglobal",
632];
633
634/// sh:180-190 — the `_comp_setup` string that every compsys entry
635/// point evals to install the option set + IFS + null stdin + no-ZERR.
636/// Bit-identical to upstream so a user-supplied `_comp_setup`
637/// override (very rare) still matches.
638pub const COMP_SETUP_EVAL: &str = concat!(
639    "local -A _comp_caller_options;\n",
640    "_comp_caller_options=(${(kv)options[@]});\n",
641    "setopt localoptions localtraps localpatterns ${_comp_options[@]};\n",
642    "local IFS=$' \\t\\r\\n\\0';\n",
643    "builtin enable -p \\| \\~ \\( \\? \\* \\[ \\< \\^ \\# 2>&-;\n",
644    "exec </dev/null;\n",
645    "trap - ZERR;\n",
646    "local -a reply;\n",
647    "local REPLY;\n",
648    "local REPORTTIME;\n",
649    "unset REPORTTIME"
650);
651
652/// sh:558 — the 8 standard ZLE widgets that compinit rebinds to
653/// `_main_complete` so any of them triggers a completion attempt.
654pub const STANDARD_COMPLETE_WIDGETS: &[&str] = &[
655    "complete-word",
656    "delete-char-or-list",
657    "expand-or-complete",
658    "expand-or-complete-prefix",
659    "list-choices",
660    "menu-complete",
661    "menu-expand-or-complete",
662    "reverse-menu-complete",
663];
664
665// =====================================================================
666// State publication helpers — keep the shell-side `$compprefuncs`
667// and `$comppostfuncs` arrays initialized empty per sh:195-197.
668// =====================================================================
669
670/// Initialize the shell-side `compprefuncs` / `comppostfuncs` arrays
671/// to empty (sh:195-197). Idempotent; safe to call from compinit
672/// before any user-side `_call_function` would have populated them.
673pub fn init_comp_funcs_arrays() {
674    crate::ported::params::setaparam("compprefuncs", Vec::new());
675    crate::ported::params::setaparam("comppostfuncs", Vec::new());
676}
677
678/// Default `$_comp_dumpfile` path (sh:129-134). User can override
679/// via `compinit -d <file>`; without that, use `${ZDOTDIR:-$HOME}`
680/// + `/.zcompdump`.
681pub fn default_dumpfile_path() -> PathBuf {
682    let home = std::env::var("ZDOTDIR")
683        .ok()
684        .filter(|s| !s.is_empty())
685        .or_else(|| std::env::var("HOME").ok())
686        .unwrap_or_else(|| ".".to_string());
687    PathBuf::from(home).join(".zcompdump")
688}
689
690/// Publish the standard ZLE rebind set to the dispatcher hooks
691/// (sh:555-560). For each `complete-word` family widget + a
692/// conditional `menu-select` (when `zsh/complist` is loaded), bind
693/// it to `_main_complete` via `zle -C`. Returns the count of
694/// successful binds.
695pub fn install_standard_complete_widgets() -> usize {
696    // `zle` is a builtin, not a shell function, so it must be invoked
697    // through the builtin entry (`bin_zle_complete`) — NOT
698    // `dispatch_function_call`, which only resolves shell functions and
699    // silently returns None for a builtin name, leaving every widget
700    // unbound (the whole compsys engine then never fires on Tab).
701    let empty_ops = crate::ported::zsh_h::options {
702        ind: [0u8; crate::ported::zsh_h::MAX_OPS],
703        args: Vec::new(),
704        argscount: 0,
705        argsalloc: 0,
706    };
707    let mut count = 0usize;
708    tracing::debug!(target: "compsys_args", "install_standard_complete_widgets ENTER");
709    for w in STANDARD_COMPLETE_WIDGETS {
710        // `zle -C <w> .<w> _main_complete` — args are post-flag:
711        // [target-thingy, base-comp-widget, completion-func].
712        let args = [
713            w.to_string(),
714            format!(".{}", w),
715            "_main_complete".to_string(),
716        ];
717        let rc_w = crate::ported::zle::zle_thingy::bin_zle_complete("zle", &args, &empty_ops, 0);
718        tracing::debug!(target: "compsys_args", widget = %w, rc_w, "zle -C standard widget");
719        if rc_w == 0 {
720            count += 1;
721        }
722    }
723    // sh:560 — `zle -C menu-select .menu-select _main_complete` (only
724    // succeeds when the `.menu-select` base widget exists, i.e.
725    // `zsh/complist` is loaded; bin_zle_complete returns 1 otherwise).
726    {
727        let args = [
728            "menu-select".to_string(),
729            ".menu-select".to_string(),
730            "_main_complete".to_string(),
731        ];
732        let rc_w = crate::ported::zle::zle_thingy::bin_zle_complete("zle", &args, &empty_ops, 0);
733        tracing::debug!(target: "compsys_args", widget = "menu-select", rc_w, "zle -C standard widget");
734        if rc_w == 0 {
735            count += 1;
736        }
737    }
738    count
739}
740
741/// sh:564-569 — when the configured `completer` chain includes
742/// `_expand` AND `^i` is currently bound to `expand-or-complete`,
743/// rebind `^i` to `complete-word` so users don't get unexpected
744/// glob expansion on TAB.
745pub fn maybe_rebind_tab_for_expand() {
746    let curcontext = crate::ported::params::getsparam("curcontext").unwrap_or_default();
747    let completers = crate::ported::modules::zutil::lookupstyle(
748        &format!(":completion:{}:", curcontext),
749        "completer",
750    );
751    let has_expand = completers
752        .iter()
753        .any(|c| c == "_expand" || c.starts_with("_expand:"));
754    if !has_expand {
755        return;
756    }
757    // sh:564 — `bindkey '^i' | IFS=$' \t' read -A _i_line`. We can't
758    //   capture bindkey output without a real executor, so just
759    //   dispatch the rebind unconditionally when _expand is in
760    //   the chain. The shell guards on `expand-or-complete` being
761    //   the current binding; without capture we'd be over-eager,
762    //   so dispatch via the hook (no-op when no executor wired).
763    let _ = crate::ported::exec::dispatch_function_call(
764        "bindkey",
765        &["^i".to_string(), "complete-word".to_string()],
766    );
767}
768
769// `compaudit` lives in its own file (`src/compsys/ported/compaudit.rs`)
770// per zsh upstream's layout (`Completion/compaudit` is a sibling of
771// `Completion/compinit`). Re-export the entry point so existing
772// compinit-facing callers don't need to change.
773pub use super::compaudit::{compaudit, CompauditError};
774
775/// Completion definition from #compdef line
776#[derive(Clone, Debug)]
777pub enum CompDef {
778    /// Regular command completion: #compdef cmd1 cmd2 ...
779    Commands(Vec<String>),
780    /// Pattern completion: #compdef -p 'pattern' [pattern...]
781    Pattern(Vec<String>),
782    /// Post-pattern completion: #compdef -P 'pattern' [pattern...]
783    PostPattern(Vec<String>),
784    /// Key binding: #compdef -k style key1 key2 ...
785    KeyBinding { style: String, keys: Vec<String> },
786    /// Widget key binding: #compdef -K widget style key
787    WidgetKey {
788        widget: String,
789        style: String,
790        key: String,
791    },
792}
793
794/// Parsed completion file
795#[derive(Clone, Debug)]
796pub struct CompFile {
797    /// Full path to the file
798    pub path: PathBuf,
799    /// Function name (filename without path)
800    pub name: String,
801    /// What this file defines
802    pub def: CompFileDef,
803    /// Full file body (read during scan for caching)
804    pub body: Option<String>,
805}
806
807/// What a completion file defines
808#[derive(Clone, Debug)]
809pub enum CompFileDef {
810    /// #compdef - completion function
811    CompDef(CompDef),
812    /// #autoload - helper function with options
813    Autoload(Vec<String>),
814    None,
815}
816
817/// Result of compinit scan
818#[derive(Debug, Default)]
819pub struct CompInitResult {
820    /// Command -> function mapping (_comps)
821    pub comps: HashMap<String, String>,
822    /// Command -> service mapping (_services)
823    pub services: HashMap<String, String>,
824    /// Pattern -> function mapping (_patcomps)
825    pub patcomps: HashMap<String, String>,
826    /// Post-pattern -> function mapping (_postpatcomps)
827    pub postpatcomps: HashMap<String, String>,
828    /// Autoload functions with options (_compautos)
829    pub compautos: HashMap<String, String>,
830    /// All scanned files
831    pub files: Vec<CompFile>,
832    /// `#compdef -k <style> <key>...` widget bindings collected from file
833    /// headers: (func, style, keys). Applied by the foreground compinit
834    /// caller via `zle -C` + `bindkey` (upstream compinit sh:356-379) —
835    /// the scan itself may run on a background thread and must not touch
836    /// keymaps.
837    pub keybindings: Vec<(String, String, Vec<String>)>,
838    /// `#compdef -K <widget> <style> <key>` triplets: (widget, style, key, func).
839    pub widgetkeys: Vec<(String, String, String, String)>,
840    /// Scan duration
841    pub scan_time_ms: u64,
842    /// Number of directories scanned
843    pub dirs_scanned: usize,
844    /// Number of files scanned
845    pub files_scanned: usize,
846}
847
848/// Parse the first line of a completion file
849///
850/// Handles all #compdef variants:
851/// - `#compdef cmd1 cmd2` - regular commands
852/// - `#compdef - cmd1 cmd2` - bare hyphen + commands (hyphen maps to '-')
853/// - `#compdef -default-` - special context entries
854/// - `#compdef -value-,VAR,-default-` - value context entries  
855/// - `#compdef -p pattern` - pattern completions
856/// - `#compdef -P pattern` - post-pattern completions
857/// - `#compdef -k style key` - key bindings
858/// - `#compdef -K widget style key` - widget key bindings
859fn parse_first_line(line: &str) -> CompFileDef {
860    let line = line.trim();
861
862    if let Some(rest) = line.strip_prefix("#compdef") {
863        let rest = rest.trim();
864        if rest.is_empty() {
865            return CompFileDef::None;
866        }
867
868        let parts: Vec<&str> = rest.split_whitespace().collect();
869        if parts.is_empty() {
870            return CompFileDef::None;
871        }
872
873        // Check for special options first
874        match parts[0] {
875            // c:compinit sh:277,387-396 — `-p pattern` sets type=pattern; every
876            // remaining word is a PATTERN key inserted into `_patcomps` (NOT
877            // `_comps`). Collect all patterns after the flag.
878            "-p" if parts.len() >= 2 => {
879                let pats: Vec<String> =
880                    parts[1..].iter().filter(|p| **p != "-p").map(|s| s.to_string()).collect();
881                if pats.is_empty() {
882                    CompFileDef::None
883                } else {
884                    CompFileDef::CompDef(CompDef::Pattern(pats))
885                }
886            }
887            // c:compinit sh:279,389-403 — `-P pattern` sets type=postpattern; every
888            // remaining word is a PATTERN key inserted into `_postpatcomps` (a
889            // post-pattern is tried AFTER normal completion). It does NOT go into
890            // `_comps`. (Previously mis-routed to `_comps` via CompDef::Commands.)
891            "-P" if parts.len() >= 2 => {
892                let pats: Vec<String> =
893                    parts[1..].iter().filter(|p| **p != "-P").map(|s| s.to_string()).collect();
894                if pats.is_empty() {
895                    CompFileDef::None
896                } else {
897                    CompFileDef::CompDef(CompDef::PostPattern(pats))
898                }
899            }
900            "-k" if parts.len() >= 3 => CompFileDef::CompDef(CompDef::KeyBinding {
901                style: parts[1].to_string(),
902                keys: parts[2..].iter().map(|s| s.to_string()).collect(),
903            }),
904            "-K" if parts.len() >= 4 => CompFileDef::CompDef(CompDef::WidgetKey {
905                widget: parts[1].to_string(),
906                style: parts[2].to_string(),
907                key: parts[3].to_string(),
908            }),
909            _ => {
910                // Parse command definitions, including:
911                // - bare "-" (maps to '-' in _comps)
912                // - context entries like "-default-", "-redirect-", "-value-,VAR,-default-"
913                // - regular commands
914                //
915                // Skip actual option flags like "-n" but keep context entries
916                let cmds: Vec<String> = parts
917                    .iter()
918                    .filter(|s| {
919                        // Keep if:
920                        // - bare hyphen "-"
921                        // - context entry like "-foo-" or "-value-,X,-default-"
922                        // - regular command (no leading hyphen)
923                        **s == "-" || is_context_entry(s) || !s.starts_with('-')
924                    })
925                    .map(|s| s.to_string())
926                    .collect();
927                if cmds.is_empty() {
928                    CompFileDef::None
929                } else {
930                    CompFileDef::CompDef(CompDef::Commands(cmds))
931                }
932            }
933        }
934    } else if let Some(rest) = line.strip_prefix("#autoload") {
935        let opts: Vec<String> = rest.split_whitespace().map(|s| s.to_string()).collect();
936        CompFileDef::Autoload(opts)
937    } else {
938        CompFileDef::None
939    }
940}
941
942/// Check if a string is a zsh completion context entry
943/// Context entries are like: -default-, -redirect-, -command-, -value-,VAR,-default-
944/// Also handles service syntax: -redirect-,<,bunzip2=bunzip2
945fn is_context_entry(s: &str) -> bool {
946    if !s.starts_with('-') {
947        return false;
948    }
949    // Strip service suffix for checking
950    let base = s.split('=').next().unwrap_or(s);
951
952    // Check if it's a known context pattern:
953    // 1. Ends with '-' like -default-, -redirect-
954    // 2. Contains comma (context specifiers like -redirect-,<,bunzip2 or -value-,VAR,-default-)
955    // 3. But NOT single letter options like -p, -P, -k, -K, -n
956    if base.len() <= 2 {
957        return base == "-"; // bare hyphen is a context entry
958    }
959
960    base.ends_with('-') || base.contains(',')
961}
962
963/// Scan a single completion file - reads full body for caching
964fn scan_file(path: &Path) -> Option<CompFile> {
965    let name = path.file_name()?.to_string_lossy().to_string();
966
967    // Must start with underscore
968    if !name.starts_with('_') {
969        return None;
970    }
971
972    // Skip certain patterns
973    if name.contains(';')
974        || name.contains('|')
975        || name.contains('&')
976        || name.ends_with('~')
977        || name.ends_with(".zwc")
978    {
979        return None;
980    }
981
982    // Read entire file at once (will be cached in SQLite)
983    let body = fs::read_to_string(path).ok()?;
984
985    // Parse first line for directive
986    let first_line = body.lines().next().unwrap_or("");
987    let def = parse_first_line(first_line);
988
989    Some(CompFile {
990        path: path.to_path_buf(),
991        name,
992        def,
993        body: Some(body),
994    })
995}
996
997/// Scan a directory for completion files (parallel)
998fn scan_directory(dir: &Path) -> Vec<CompFile> {
999    let entries = match fs::read_dir(dir) {
1000        Ok(e) => e,
1001        Err(_) => return Vec::new(),
1002    };
1003
1004    let paths: Vec<PathBuf> = entries
1005        .filter_map(|e| e.ok())
1006        .map(|e| e.path())
1007        .filter(|p| p.is_file())
1008        .collect();
1009
1010    // Parallel scan of files within directory
1011    paths.par_iter().filter_map(|p| scan_file(p)).collect()
1012}
1013
1014/// Initialize the completion system by scanning fpath
1015///
1016/// This is the main entry point - replaces the zsh compinit function.
1017/// Uses rayon for parallel directory and file scanning.
1018/// Apply the `#compdef -k`/`-K` widget key bindings a scan collected —
1019/// the in-shell half the scan can't do itself (it may run on a background
1020/// thread). Mirrors compdef's key branch: sh:365 `zle -C <func> .<style>
1021/// <func>`, sh:378 `bindkey <key> <func>`. Without this, header-declared
1022/// completion widgets parsed but never bound — `^X?` (_complete_debug,
1023/// `#compdef -k complete-word \C-x?`) and `^Xh` (_complete_help)
1024/// self-inserted literally instead of running.
1025pub fn apply_keybindings(result: &CompInitResult) {
1026    for (func, style, keys) in &result.keybindings {
1027        for key in keys {
1028            install_comp_keybinding(func, style, key, func);
1029        }
1030    }
1031    for (widget, style, key, func) in &result.widgetkeys {
1032        install_comp_keybinding(widget, style, key, func);
1033    }
1034}
1035
1036/// One `#compdef -k`/`-K` binding: sh:346/365 `zle -C <widget> .<style>
1037/// <func>` + sh:351/378 `bindkey <key> <widget>`. Goes through the builtin
1038/// entries directly — `dispatch_function_call` resolves only shell
1039/// functions and silently no-ops for builtins (see
1040/// install_standard_complete_widgets).
1041fn install_comp_keybinding(widget: &str, style: &str, key: &str, func: &str) {
1042    let empty_ops = crate::ported::zsh_h::options {
1043        ind: [0u8; crate::ported::zsh_h::MAX_OPS],
1044        args: Vec::new(),
1045        argscount: 0,
1046        argsalloc: 0,
1047    };
1048    let style_dotted = if style.starts_with('.') {
1049        style.to_string()
1050    } else {
1051        format!(".{}", style)
1052    };
1053    // sh:346/365 — `zle -C <widget> <.style> <func>`.
1054    let zle_args = [widget.to_string(), style_dotted, func.to_string()];
1055    let _ = crate::ported::zle::zle_thingy::bin_zle_complete("zle", &zle_args, &empty_ops, 0);
1056    // sh:351/378 — `bindkey <key> <widget>`.
1057    let bk_args = [key.to_string(), widget.to_string()];
1058    let _ = crate::ported::zle::zle_keymap::bin_bindkey("bindkey", &bk_args, &empty_ops, 0);
1059}
1060
1061/// The `#compdef -k`/`-K` headers shipped in the upstream Completion tree —
1062/// the fixed set compinit produces for a stock install (each cited from its
1063/// file's first line). Installed synchronously with the standard widget
1064/// rebind because the background scan's results merge lazily (and the
1065/// cached path skips the scan entirely); user fpath files with -k headers
1066/// are additionally collected by the scan into CompInitResult.keybindings.
1067const STANDARD_COMP_KEYBINDINGS: &[(&str, &str, &str, &str)] = &[
1068    // (widget, style, key, func)
1069    ("_complete_debug", "complete-word", "\u{18}?", "_complete_debug"), // Base/Widget/_complete_debug:1 \C-x?
1070    ("_complete_help", "complete-word", "\u{18}h", "_complete_help"),   // Base/Widget/_complete_help:1 \C-xh
1071    ("_complete_tag", "complete-word", "\u{18}t", "_complete_tag"),     // Base/Widget/_complete_tag:1 \C-xt
1072    ("_correct_filename", "complete-word", "\u{18}C", "_correct_filename"), // _correct_filename:1 \C-xC
1073    ("_correct_word", "complete-word", "\u{18}c", "_correct_word"),     // _correct_word:1 \C-xc
1074    ("_read_comp", "complete-word", "\u{18}\u{12}", "_read_comp"),      // _read_comp:1 \C-x\C-r
1075    ("_most_recent_file", "complete-word", "\u{18}m", "_most_recent_file"), // _most_recent_file:1 \C-xm
1076    ("_next_tags", "list-choices", "\u{18}n", "_next_tags"),            // _next_tags:1 \C-xn
1077    ("_expand_word", "complete-word", "\u{18}e", "_expand_word"),       // _expand_word:1 -K (1st pair)
1078    ("_list_expansions", "list-choices", "\u{18}d", "_expand_word"),    // _expand_word:1 -K (2nd pair)
1079    ("_bash_complete-word", "complete-word", "\u{1b}~", "_bash_completions"), // _bash_completions:1 -K
1080    ("_bash_list-choices", "list-choices", "\u{18}~", "_bash_completions"),   // _bash_completions:1 -K
1081    ("_history-complete-older", "complete-word", "\u{1b}/", "_history_complete_word"), // _history_complete_word:1 -K
1082    ("_history-complete-newer", "complete-word", "\u{1b},", "_history_complete_word"), // _history_complete_word:1 -K
1083    ("_expand_alias", "complete-word", "\u{18}a", "_expand_alias"),     // Base/Completer/_expand_alias:1 -K
1084];
1085
1086/// Install the stock `#compdef -k`/`-K` bindings (see
1087/// STANDARD_COMP_KEYBINDINGS). Runs on the main thread next to
1088/// install_standard_complete_widgets.
1089pub fn install_standard_comp_keybindings() {
1090    for (widget, style, key, func) in STANDARD_COMP_KEYBINDINGS {
1091        install_comp_keybinding(widget, style, key, func);
1092    }
1093}
1094
1095pub fn compinit(fpath: &[PathBuf]) -> CompInitResult {
1096    let start = Instant::now();
1097
1098    // sh:129-134 — install `$_comp_dumpfile` default so engine code
1099    //   that calls `getsparam("_comp_dumpfile")` sees a sensible
1100    //   path. User-supplied `compinit -d FILE` overrides.
1101    if crate::ported::params::getsparam("_comp_dumpfile")
1102        .map(|s| s.is_empty())
1103        .unwrap_or(true)
1104    {
1105        let _ = crate::ported::params::setsparam(
1106            "_comp_dumpfile",
1107            &default_dumpfile_path().to_string_lossy(),
1108        );
1109    }
1110
1111    // sh:138-172 — publish `_comp_options` to the shell-side param
1112    //   table so `$_comp_setup` eval at every entry point picks it
1113    //   up. Use the canonical const list.
1114    crate::ported::params::setaparam(
1115        "_comp_options",
1116        COMP_OPTIONS.iter().map(|s| s.to_string()).collect(),
1117    );
1118
1119    // sh:180-190 — publish `_comp_setup` (the eval string).
1120    let _ = crate::ported::params::setsparam("_comp_setup", COMP_SETUP_EVAL);
1121
1122    // sh:195-197 — initialize the pre/post-hook arrays.
1123    init_comp_funcs_arrays();
1124
1125    // Track seen function names (first one wins)
1126    let seen: Mutex<HashSet<String>> = Mutex::new(HashSet::new());
1127
1128    // Parallel scan of all directories
1129    let all_files: Vec<CompFile> = fpath
1130        .par_iter()
1131        .filter(|dir| dir.as_os_str() != "." && dir.exists())
1132        .flat_map(|dir| scan_directory(dir))
1133        .filter(|f| {
1134            let mut seen = seen.lock().unwrap();
1135            if seen.contains(&f.name) {
1136                false
1137            } else {
1138                seen.insert(f.name.clone());
1139                true
1140            }
1141        })
1142        .collect();
1143
1144    let files_scanned = all_files.len();
1145    let dirs_scanned = fpath.len();
1146
1147    // Build the result maps
1148    let mut result = CompInitResult {
1149        scan_time_ms: start.elapsed().as_millis() as u64,
1150        dirs_scanned,
1151        files_scanned,
1152        ..Default::default()
1153    };
1154
1155    for file in &all_files {
1156        match &file.def {
1157            CompFileDef::CompDef(compdef) => {
1158                match compdef {
1159                    CompDef::Commands(cmds) => {
1160                        for cmd in cmds {
1161                            // Handle service syntax: cmd=service
1162                            if let Some(eq_pos) = cmd.find('=') {
1163                                let cmd_name = &cmd[..eq_pos];
1164                                let service = &cmd[eq_pos + 1..];
1165                                result.comps.insert(cmd_name.to_string(), file.name.clone());
1166                                result
1167                                    .services
1168                                    .insert(cmd_name.to_string(), service.to_string());
1169                            } else {
1170                                result.comps.insert(cmd.clone(), file.name.clone());
1171                            }
1172                        }
1173                    }
1174                    CompDef::Pattern(pats) => {
1175                        // c:compinit sh:396 — `_patcomps[$1]="$func"`. Pattern
1176                        // compdefs go to `_patcomps` ONLY, never `_comps`.
1177                        for pat in pats {
1178                            result.patcomps.insert(pat.clone(), file.name.clone());
1179                        }
1180                    }
1181                    CompDef::PostPattern(pats) => {
1182                        // c:compinit sh:403 — `_postpatcomps[$1]="$func"`.
1183                        for pat in pats {
1184                            result.postpatcomps.insert(pat.clone(), file.name.clone());
1185                        }
1186                    }
1187                    CompDef::KeyBinding { style, keys } => {
1188                        // sh:356-379 — `#compdef -k <style> <keys…>`: the widget
1189                        // takes the FILE's name (e.g. _complete_debug). Collected
1190                        // here; zle -C + bindkey happen in apply_keybindings
1191                        // (this scan may run on a background thread).
1192                        result.keybindings.push((
1193                            file.name.clone(),
1194                            style.clone(),
1195                            keys.clone(),
1196                        ));
1197                    }
1198                    CompDef::WidgetKey {
1199                        widget,
1200                        style,
1201                        key,
1202                    } => {
1203                        // sh:336-354 — `#compdef -K <widget> <style> <key>`:
1204                        // the completion FUNCTION is the file's name, the
1205                        // widget name is explicit.
1206                        result.widgetkeys.push((
1207                            widget.clone(),
1208                            style.clone(),
1209                            key.clone(),
1210                            file.name.clone(),
1211                        ));
1212                    }
1213                }
1214            }
1215            CompFileDef::Autoload(opts) => {
1216                let opts_str = opts.join(" ");
1217                result.compautos.insert(file.name.clone(), opts_str);
1218            }
1219            CompFileDef::None => {}
1220        }
1221    }
1222
1223    result.files = all_files;
1224
1225    // sh:553-569 — the standard completion-widget rebind (`zle -C
1226    //   complete-word .complete-word _main_complete` × 8 + conditional
1227    //   menu-select + TAB/_expand rebind) is NOT done here. `compinit`
1228    //   ships this fpath scan to a worker-pool thread (see
1229    //   `ext_builtins::builtin_compinit`), and ZLE keymaps/widgets live
1230    //   on the main thread — a `zle -C` issued from a worker never
1231    //   reaches the interactive keymap, so TAB stays bound to the
1232    //   builtin `expand-or-complete` and `_main_complete` never fires.
1233    //   The rebind is instead performed synchronously on the main
1234    //   thread in `builtin_compinit`, where it belongs; it needs only
1235    //   `_main_complete` (a Rust fn, always present), not the scan
1236    //   results, so deferring it to the background is unnecessary.
1237
1238    // Publish the result-side compdef state so downstream `getaparam(
1239    //   "_comps")` etc. queries reflect the scan. This is the new-
1240    //   compinit-finish complement of `compdef()`'s per-call publish.
1241    with_state(|s| {
1242        for (k, v) in &result.comps {
1243            s.comps.insert(k.clone(), v.clone());
1244        }
1245        for (k, v) in &result.services {
1246            s.services.insert(k.clone(), v.clone());
1247        }
1248        for (k, v) in &result.patcomps {
1249            s.patcomps.insert(k.clone(), v.clone());
1250        }
1251        for (k, v) in &result.postpatcomps {
1252            s.postpatcomps.insert(k.clone(), v.clone());
1253        }
1254        for (k, v) in &result.compautos {
1255            s.compautos.insert(k.clone(), v.clone());
1256        }
1257        publish_compdef_state(s);
1258    });
1259
1260    result
1261}
1262
1263// `compdump`, `check_dump`, and `escape_zsh_string` moved to
1264// `compsys/ported/compdump.rs` (1:1 with upstream `Completion/compdump`).
1265pub use super::compdump::{check_dump, compdump};
1266
1267/// Build SQLite cache from fpath scan
1268///
1269/// This is the main entry point for initializing the completion system.
1270/// It scans fpath directories, parses #compdef directives, and populates
1271/// the SQLite cache for fast lookups.
1272pub fn build_cache_from_fpath(
1273    fpath: &[PathBuf],
1274    cache: &mut crate::compsys::cache::CompsysCache,
1275) -> std::io::Result<CompInitResult> {
1276    use std::time::Instant;
1277
1278    let t0 = Instant::now();
1279    let result = compinit(fpath);
1280    let scan_time = t0.elapsed();
1281
1282    let t1 = Instant::now();
1283
1284    // Populate comps table (_comps hash)
1285    let comps: Vec<(String, String)> = result
1286        .comps
1287        .iter()
1288        .map(|(k, v)| (k.clone(), v.clone()))
1289        .collect();
1290    cache
1291        .set_comps_bulk(&comps)
1292        .map_err(|e| std::io::Error::other(e.to_string()))?;
1293
1294    // Populate services table (_services hash)
1295    let services: Vec<(String, String)> = result
1296        .services
1297        .iter()
1298        .map(|(k, v)| (k.clone(), v.clone()))
1299        .collect();
1300    cache
1301        .set_services_bulk(&services)
1302        .map_err(|e| std::io::Error::other(e.to_string()))?;
1303
1304    // Populate patcomps table (_patcomps hash)
1305    for (pattern, function) in &result.patcomps {
1306        cache
1307            .set_patcomp(pattern, function)
1308            .map_err(|e| std::io::Error::other(e.to_string()))?;
1309    }
1310
1311    // Populate postpatcomps (stored in patcomps with a marker, or separate table if needed)
1312    // For now, we'll merge them into patcomps
1313    for (pattern, function) in &result.postpatcomps {
1314        cache
1315            .set_patcomp(pattern, function)
1316            .map_err(|e| std::io::Error::other(e.to_string()))?;
1317    }
1318
1319    let comps_time = t1.elapsed();
1320    let t2 = Instant::now();
1321
1322    // Populate autoloads table with function bodies for instant loading
1323    // Bodies were already read during parallel scan - no extra I/O here
1324    let autoloads: Vec<(String, String, String)> = result
1325        .files
1326        .iter()
1327        .filter(|f| matches!(f.def, CompFileDef::CompDef(_) | CompFileDef::Autoload(_)))
1328        .filter_map(|f| {
1329            let path_str = f.path.to_string_lossy().to_string();
1330            let body = f.body.as_ref()?.clone();
1331            Some((f.name.clone(), path_str, body))
1332        })
1333        .collect();
1334    cache
1335        .add_autoloads_with_bodies_bulk(&autoloads)
1336        .map_err(|e| std::io::Error::other(e.to_string()))?;
1337
1338    let autoloads_time = t2.elapsed();
1339
1340    // Timing logged by caller in vm_helper via tracing
1341
1342    Ok(result)
1343}
1344
1345/// Load _comps from existing cache (instantaneous)
1346///
1347/// Returns a CompInitResult populated from the SQLite cache without rescanning fpath.
1348/// Use this after the cache has been built with `build_cache_from_fpath`.
1349///
1350/// This is the equivalent of `compinit -C` with a valid zcompdump - it skips
1351/// the fpath scan entirely and just loads from cache.
1352#[allow(clippy::field_reassign_with_default)] // result is mutated across many subsequent statements; struct-literal init not practical
1353pub fn load_from_cache(
1354    cache: &crate::compsys::cache::CompsysCache,
1355) -> std::io::Result<CompInitResult> {
1356    use std::time::Instant;
1357    let start = Instant::now();
1358
1359    let mut result = CompInitResult::default();
1360
1361    // Load comps - single query
1362    result.comps = cache
1363        .get_all_comps()
1364        .map_err(|e| std::io::Error::other(e.to_string()))?;
1365
1366    // Load patcomps - single query
1367    for (pat, func) in cache
1368        .patcomps_kv()
1369        .map_err(|e| std::io::Error::other(e.to_string()))?
1370    {
1371        result.patcomps.insert(pat, func);
1372    }
1373
1374    // Services are loaded on-demand via cache.get_service() - no need to preload
1375    // This matches zsh behavior where $_services is lazily populated
1376
1377    result.scan_time_ms = start.elapsed().as_millis() as u64;
1378    result.files_scanned = result.comps.len();
1379
1380    Ok(result)
1381}
1382
1383/// Fast check if compinit is needed
1384///
1385/// Returns the number of completion entries in cache, or 0 if cache is empty/invalid.
1386/// Use this to decide whether to run full compinit or load_from_cache.
1387pub fn cache_entry_count(cache: &crate::compsys::cache::CompsysCache) -> usize {
1388    cache.comp_count().unwrap_or(0) as usize
1389}
1390
1391/// Lazy compinit - validates cache exists but doesn't load into memory
1392///
1393/// This is the fastest option for shell startup. It just verifies the cache
1394/// is valid and returns immediately. Actual lookups happen via cache.get_comp().
1395///
1396/// Returns (is_valid, entry_count) in microseconds.
1397pub fn compinit_lazy(cache: &crate::compsys::cache::CompsysCache) -> (bool, usize) {
1398    let count = cache.comp_count().unwrap_or(0) as usize;
1399    (count > 0, count)
1400}
1401
1402/// Check if cache is valid and up-to-date
1403///
1404/// Returns true if cache exists and has entries, false if cache needs to be rebuilt.
1405pub fn cache_is_valid(cache: &crate::compsys::cache::CompsysCache) -> bool {
1406    cache.comp_count().unwrap_or(0) > 0
1407}
1408
1409/// Get system fpath from environment or defaults
1410pub fn get_system_fpath() -> Vec<PathBuf> {
1411    // Try FPATH env var first
1412    if let Ok(fpath_str) = std::env::var("FPATH") {
1413        if !fpath_str.is_empty() {
1414            return fpath_str.split(':').map(PathBuf::from).collect();
1415        }
1416    }
1417
1418    // Default paths for common systems
1419    let mut paths = Vec::new();
1420
1421    // macOS Homebrew
1422    for base in &["/opt/homebrew", "/usr/local"] {
1423        paths.push(PathBuf::from(format!("{}/share/zsh/site-functions", base)));
1424        paths.push(PathBuf::from(format!("{}/share/zsh/functions", base)));
1425    }
1426
1427    // System zsh
1428    for version in &["5.9", "5.8", "5.7"] {
1429        paths.push(PathBuf::from(format!(
1430            "/usr/share/zsh/{}/functions",
1431            version
1432        )));
1433    }
1434    paths.push(PathBuf::from("/usr/share/zsh/functions"));
1435    paths.push(PathBuf::from("/usr/share/zsh/site-functions"));
1436
1437    // Zinit/zplugin common paths
1438    if let Ok(home) = std::env::var("HOME") {
1439        paths.push(PathBuf::from(format!("{}/.zinit/completions", home)));
1440        paths.push(PathBuf::from(format!("{}/.zplugin/completions", home)));
1441        paths.push(PathBuf::from(format!(
1442            "{}/.local/share/zsh/site-functions",
1443            home
1444        )));
1445    }
1446
1447    // Filter to existing directories
1448    paths.into_iter().filter(|p| p.exists()).collect()
1449}
1450
1451/// Options for compinit
1452#[derive(Clone, Debug, Default)]
1453pub struct CompInitOpts {
1454    /// Dump file path (-d)
1455    pub dump_file: Option<PathBuf>,
1456    /// Skip dump (-D)
1457    pub no_dump: bool,
1458    /// Skip security check (-C)
1459    pub no_check: bool,
1460    /// Ignore insecure dirs (-i)
1461    pub ignore_insecure: bool,
1462    /// Use insecure dirs (-u)
1463    pub use_insecure: bool,
1464}
1465
1466impl CompInitOpts {
1467    /// Parse compinit arguments
1468    pub fn parse(args: &[String]) -> Self {
1469        let mut opts = Self::default();
1470        let mut i = 0;
1471
1472        while i < args.len() {
1473            match args[i].as_str() {
1474                "-d" if i + 1 < args.len() && !args[i + 1].starts_with('-') => {
1475                    opts.dump_file = Some(PathBuf::from(&args[i + 1]));
1476                    i += 1;
1477                }
1478                "-D" => opts.no_dump = true,
1479                "-C" => opts.no_check = true,
1480                "-i" => opts.ignore_insecure = true,
1481                "-u" => opts.use_insecure = true,
1482                _ => {}
1483            }
1484            i += 1;
1485        }
1486
1487        opts
1488    }
1489}
1490
1491// =====================================================================
1492// compdef() — runtime registration entry point.
1493//
1494// Upstream defines this inside `Completion/compinit` (sh:253-446); we
1495// mirror that organizational choice. User `.zshrc` lines like:
1496//   compdef _git git
1497//   compdef -p '*-test' _test
1498//   compdef -d obsolete
1499// land here.
1500//
1501// State lives in a session-side `CompdefState` (a Mutex<HashMap>
1502// quintet for `_comps`, `_services`, `_patcomps`, `_postpatcomps`,
1503// `_compautos`). Cluster ports already read these via shell-side
1504// `getaparam("_comps")` etc.; the published-to-paramtab step
1505// happens in `publish_compdef_state` at the bottom of this section.
1506// =====================================================================
1507
1508/// Session-side compdef registrations. Mirrors the five upstream
1509/// assoc arrays one-for-one.
1510#[derive(Default)]
1511pub struct CompdefState {
1512    pub comps: HashMap<String, String>,
1513    pub services: HashMap<String, String>,
1514    pub patcomps: HashMap<String, String>,
1515    pub postpatcomps: HashMap<String, String>,
1516    pub compautos: HashMap<String, String>,
1517}
1518
1519static COMPDEF_STATE: Mutex<Option<CompdefState>> = Mutex::new(None);
1520
1521/// Lock the session-side state, initializing on first call.
1522fn with_state<F, R>(f: F) -> R
1523where
1524    F: FnOnce(&mut CompdefState) -> R,
1525{
1526    let mut guard = COMPDEF_STATE.lock().unwrap();
1527    if guard.is_none() {
1528        *guard = Some(CompdefState::default());
1529    }
1530    f(guard.as_mut().unwrap())
1531}
1532
1533/// Helper to publish state inside a `with_state` closure (takes
1534/// `&mut` to fit the closure signature).
1535fn publish_compdef_state_mut(s: &mut CompdefState) {
1536    publish_compdef_state(s);
1537}
1538
1539/// Publish the in-memory state back to shell-side assoc arrays so
1540/// engine cluster code (which reads via `getaparam("_comps")` etc.)
1541/// sees the updates. Flat key/value layout per the existing
1542/// per-engine-port convention.
1543fn publish_compdef_state(s: &CompdefState) {
1544    let mut flatten = |arr: &HashMap<String, String>| -> Vec<String> {
1545        let mut sorted: Vec<(&String, &String)> = arr.iter().collect();
1546        sorted.sort_by(|a, b| a.0.cmp(b.0));
1547        let mut out = Vec::with_capacity(sorted.len() * 2);
1548        for (k, v) in sorted {
1549            out.push(k.clone());
1550            out.push(v.clone());
1551        }
1552        out
1553    };
1554    // These are ASSOCIATIVE arrays in zsh (`typeset -gHA _comps _services
1555    // _patcomps _postpatcomps` / `_compautos`, sh:116/121). They MUST be
1556    // published via `sethparam` (hashed) — `flatten` already emits the
1557    // `[k, v, k, v, …]` pair layout `sethparam` consumes. Using `setaparam`
1558    // (plain array) made `_comps` a flat array, so `${_comps[ls]}` couldn't
1559    // hash-lookup and `_complete`/`_dispatch` found no completer for ANY
1560    // command → every `<cmd> <TAB>` (incl. `ls -<TAB>`) just rang the bell.
1561    // The synchronous `compinit -C` path used `set_assoc` (→ sethparam) and
1562    // worked, which is why only the fresh/compdef-driven path was broken.
1563    // Bug #655.
1564    crate::ported::params::sethparam("_comps", flatten(&s.comps));
1565    crate::ported::params::sethparam("_services", flatten(&s.services));
1566    crate::ported::params::sethparam("_patcomps", flatten(&s.patcomps));
1567    crate::ported::params::sethparam("_postpatcomps", flatten(&s.postpatcomps));
1568    crate::ported::params::sethparam("_compautos", flatten(&s.compautos));
1569}
1570
1571/// Parse `compdef`'s short-option flags via the upstream
1572/// `getopts "anpPkKde"` (sh:267).
1573#[derive(Default, Debug)]
1574struct CompdefFlags {
1575    autol: bool,
1576    new: bool,
1577    delete: bool,
1578    eval: bool,
1579    /// Mutually-exclusive `-p`/`-P`/`-k`/`-K`. Only the most-recent
1580    /// wins; upstream errors on duplicate, we tolerate.
1581    spec_type: SpecType,
1582}
1583
1584#[derive(Default, Debug, PartialEq, Clone, Copy)]
1585enum SpecType {
1586    #[default]
1587    Normal,
1588    Pattern,
1589    PostPattern,
1590    Key,
1591    WidgetKey,
1592}
1593
1594fn parse_compdef_flags(args: &[String]) -> Result<(CompdefFlags, usize), String> {
1595    let mut flags = CompdefFlags::default();
1596    let mut idx = 0usize;
1597    while idx < args.len() {
1598        let a = &args[idx];
1599        if !a.starts_with('-') || a == "-" || a == "--" {
1600            break;
1601        }
1602        // sh:267 getopts allows combined flags like `-an`. Walk each
1603        //   letter after the leading `-`.
1604        for c in a.chars().skip(1) {
1605            match c {
1606                'a' => flags.autol = true,
1607                'n' => flags.new = true,
1608                'd' => flags.delete = true,
1609                'e' => flags.eval = true,
1610                'p' => flags.spec_type = SpecType::Pattern,
1611                'P' => flags.spec_type = SpecType::PostPattern,
1612                'k' => flags.spec_type = SpecType::Key,
1613                'K' => flags.spec_type = SpecType::WidgetKey,
1614                _ => return Err(format!("compdef: unknown option: -{}", c)),
1615            }
1616        }
1617        idx += 1;
1618    }
1619    Ok((flags, idx))
1620}
1621
1622/// `compdef` — register or unregister completion functions for
1623/// commands. Faithful to upstream `Completion/compinit` sh:253-446.
1624///
1625/// Returns the upstream-compatible exit code: 0 on success, 1 on
1626/// usage error.
1627pub fn compdef(args: &[String]) -> i32 {
1628    // sh:262
1629    if args.is_empty() {
1630        eprintln!("compdef: I need arguments");
1631        return 1;
1632    }
1633    let (flags, mut idx) = match parse_compdef_flags(args) {
1634        Ok(p) => p,
1635        Err(e) => {
1636            eprintln!("{}", e);
1637            return 1;
1638        }
1639    };
1640    // sh:293
1641    if idx >= args.len() {
1642        eprintln!("compdef: I need arguments");
1643        return 1;
1644    }
1645
1646    if flags.delete {
1647        // sh:426-444  -d: delete by name from the right hash
1648        let names = &args[idx..];
1649        with_state(|s| match flags.spec_type {
1650            SpecType::Pattern => {
1651                for n in names {
1652                    s.patcomps.remove(n);
1653                }
1654            }
1655            SpecType::PostPattern => {
1656                for n in names {
1657                    s.postpatcomps.remove(n);
1658                }
1659            }
1660            SpecType::Key | SpecType::WidgetKey => {
1661                eprintln!("compdef: cannot restore key bindings");
1662            }
1663            SpecType::Normal => {
1664                for n in names {
1665                    s.comps.remove(n);
1666                    s.services.remove(n);
1667                }
1668            }
1669        });
1670        with_state(publish_compdef_state_mut);
1671        return 0;
1672    }
1673
1674    // sh:298-327  service-alias mode: no flags + first arg contains `=`.
1675    //   Each subsequent arg must also contain `=` (else it's an error).
1676    if !flags.eval && args[idx].contains('=') {
1677        let mut ret: i32 = 0;
1678        while idx < args.len() {
1679            let entry = args[idx].clone();
1680            idx += 1;
1681            if !entry.contains('=') {
1682                eprintln!("compdef: invalid argument: {}", entry);
1683                ret = 1;
1684                continue;
1685            }
1686            let mut sp = entry.splitn(2, '=');
1687            let cmd = sp.next().unwrap_or("").to_string();
1688            let svc_in = sp.next().unwrap_or("").to_string();
1689            // sh:307-311 — resolve `$svc` and look up its completion
1690            //   function. zsh reads the `_comps`/`_services` PARAMETERS
1691            //   (`func="$_comps[...]"`), which are the source of truth:
1692            //   the `compinit -C` dump-source path and third-party plugins
1693            //   populate the parameters directly, while the internal
1694            //   `s.comps` state only sees native `compdef` calls — so it
1695            //   lags and made `compdef func=cmd` wrongly report
1696            //   "unknown command or service" even though `$_comps[cmd]`
1697            //   was set. Read the parameters first, fall back to s.comps.
1698            let comps_param =
1699                crate::ported::subst::assoc_get("_comps").unwrap_or_default();
1700            // sh:307 — `${_services[(r)$svc]:-$svc}`. `(r)` returns the
1701            //   matching VALUE (== $svc for a literal), else the `:-$svc`
1702            //   default, so the effective service key is `$svc` itself.
1703            let resolved_svc = svc_in.clone();
1704            let func = comps_param
1705                .get(&resolved_svc)
1706                .filter(|f| !f.is_empty())
1707                .cloned()
1708                .or_else(|| {
1709                    with_state(|s| s.comps.get(&resolved_svc).cloned())
1710                        .filter(|f| !f.is_empty())
1711                })
1712                .or_else(|| {
1713                    // sh:311 fallback to first matching pat/postpat key,
1714                    //   again preferring the parameters over s.* state.
1715                    let pat = crate::ported::subst::assoc_get("_patcomps")
1716                        .unwrap_or_default();
1717                    let postpat = crate::ported::subst::assoc_get("_postpatcomps")
1718                        .unwrap_or_default();
1719                    pat.iter()
1720                        .find(|(k, _)| pattern_matches(k, &svc_in))
1721                        .map(|(_, v)| v.clone())
1722                        .or_else(|| {
1723                            postpat
1724                                .iter()
1725                                .find(|(k, _)| pattern_matches(k, &svc_in))
1726                                .map(|(_, v)| v.clone())
1727                        })
1728                        .or_else(|| {
1729                            with_state(|s| {
1730                                s.patcomps
1731                                    .iter()
1732                                    .find(|(k, _)| pattern_matches(k, &svc_in))
1733                                    .map(|(_, v)| v.clone())
1734                                    .or_else(|| {
1735                                        s.postpatcomps
1736                                            .iter()
1737                                            .find(|(k, _)| pattern_matches(k, &svc_in))
1738                                            .map(|(_, v)| v.clone())
1739                                    })
1740                            })
1741                        })
1742                })
1743                .unwrap_or_default();
1744            if func.is_empty() {
1745                eprintln!("compdef: unknown command or service: {}", svc_in);
1746                ret = 1;
1747                continue;
1748            }
1749            // sh:308-309 — `[[ -n ${_services[$svc]} ]] && svc=${_services[$svc]}`.
1750            let services_param =
1751                crate::ported::subst::assoc_get("_services").unwrap_or_default();
1752            let svc_for_state = services_param
1753                .get(&svc_in)
1754                .filter(|v| !v.is_empty())
1755                .cloned()
1756                .or_else(|| with_state(|s| s.services.get(&svc_in).cloned()))
1757                .unwrap_or(svc_in.clone());
1758            with_state(|s| {
1759                s.comps.insert(cmd.clone(), func.clone());
1760                s.services.insert(cmd, svc_for_state);
1761            });
1762        }
1763        with_state(publish_compdef_state_mut);
1764        return ret;
1765    }
1766
1767    // sh:332-334  First positional after flags is the function name.
1768    let func = args[idx].clone();
1769    idx += 1;
1770
1771    // sh:333  `-a` → autoload
1772    if flags.autol && func.starts_with('_') {
1773        // dispatch `autoload -rUz <func>` via the exec accessors bridge.
1774        let _ = crate::ported::exec::dispatch_function_call(
1775            "autoload",
1776            &["-rUz".to_string(), func.clone()],
1777        );
1778        // Track for the dump file
1779        with_state(|s| {
1780            s.compautos.insert(func.clone(), "-rUz".to_string());
1781        });
1782    }
1783
1784    // sh:336-425
1785    match flags.spec_type {
1786        SpecType::WidgetKey => {
1787            // sh:337-355  -K widget-name comp-widget key  (in triples)
1788            let mut i = idx;
1789            while i + 2 < args.len() {
1790                let mut wname = args[i].clone();
1791                let mut comp_widget = args[i + 1].clone();
1792                let key = args[i + 2].clone();
1793                if !wname.starts_with('_') {
1794                    wname = format!("_{}", wname);
1795                }
1796                if !comp_widget.starts_with('.') {
1797                    comp_widget = format!(".{}", comp_widget);
1798                }
1799                // sh:346 `zle -C` + sh:347-352 `bindkey` — through the
1800                // BUILTIN entries; dispatch_function_call resolves only
1801                // shell functions and silently no-ops for builtins, which
1802                // left every `#compdef -K` widget (^Xe _expand_word,
1803                // \e/ _history-complete-older, …) unbound.
1804                install_comp_keybinding(&wname, &comp_widget, &key, &func);
1805                i += 3;
1806            }
1807        }
1808        SpecType::Key => {
1809            // sh:356-379  -k style key... (in 1+ pairs)
1810            if idx >= args.len() {
1811                eprintln!("compdef: missing keys");
1812                return 1;
1813            }
1814            let mut style = args[idx].clone();
1815            idx += 1;
1816            if !style.starts_with('.') {
1817                style = format!(".{}", style);
1818            }
1819            // sh:365 `zle -C` + sh:373-378 `bindkey` — through the BUILTIN
1820            // entries (see the -K arm above): the dispatch_function_call
1821            // spelling silently no-op'd, so `#compdef -k` widgets — ^X?
1822            // _complete_debug, ^Xh _complete_help — never bound and the
1823            // keys self-inserted literally.
1824            for key in &args[idx..] {
1825                install_comp_keybinding(&func, &style, key, &func);
1826            }
1827        }
1828        _ => {
1829            // sh:381-424  normal / pattern / postpattern
1830            let mut effective_type = flags.spec_type;
1831            while idx < args.len() {
1832                let arg = args[idx].clone();
1833                idx += 1;
1834                // sh:385-390  inline type switch
1835                match arg.as_str() {
1836                    "-N" => {
1837                        effective_type = SpecType::Normal;
1838                        continue;
1839                    }
1840                    "-p" => {
1841                        effective_type = SpecType::Pattern;
1842                        continue;
1843                    }
1844                    "-P" => {
1845                        effective_type = SpecType::PostPattern;
1846                        continue;
1847                    }
1848                    _ => {}
1849                }
1850                with_state(|s| match effective_type {
1851                    SpecType::Pattern => {
1852                        // sh:393-398 — `key=val` rewrites to `=val=func`
1853                        if let Some(eq) = arg.find('=') {
1854                            let key = arg[..eq].to_string();
1855                            let val = arg[eq + 1..].to_string();
1856                            s.patcomps.insert(key, format!("={}={}", val, func));
1857                        } else {
1858                            s.patcomps.insert(arg.clone(), func.clone());
1859                        }
1860                    }
1861                    SpecType::PostPattern => {
1862                        if let Some(eq) = arg.find('=') {
1863                            let key = arg[..eq].to_string();
1864                            let val = arg[eq + 1..].to_string();
1865                            s.postpatcomps.insert(key, format!("={}={}", val, func));
1866                        } else {
1867                            s.postpatcomps.insert(arg.clone(), func.clone());
1868                        }
1869                    }
1870                    _ => {
1871                        // sh:407-419  normal: cmd or cmd=svc
1872                        let (cmd, svc) = if let Some(eq) = arg.find('=') {
1873                            (arg[..eq].to_string(), Some(arg[eq + 1..].to_string()))
1874                        } else {
1875                            (arg.clone(), None)
1876                        };
1877                        // sh:415  -n: no-clobber
1878                        if flags.new && s.comps.contains_key(&cmd) {
1879                            return;
1880                        }
1881                        s.comps.insert(cmd.clone(), func.clone());
1882                        if let Some(svc) = svc {
1883                            s.services.insert(cmd, svc);
1884                        }
1885                    }
1886                });
1887            }
1888        }
1889    }
1890    with_state(publish_compdef_state_mut);
1891    0
1892}
1893
1894/// sh:311 pattern-matching helper. Uses the real `pattern.rs`
1895/// matcher so `(K)` assoc-key glob matching is faithful.
1896fn pattern_matches(pat: &str, s: &str) -> bool {
1897    match crate::ported::pattern::patcompile(
1898        &{
1899            let mut __pat_tok = (pat).to_string();
1900            crate::ported::glob::tokenize(&mut __pat_tok);
1901            __pat_tok
1902        },
1903        0,
1904        None,
1905    ) {
1906        Some(prog) => crate::ported::pattern::pattry(&prog, s),
1907        None => pat == s,
1908    }
1909}
1910
1911/// Reset session-side state (test-only helper; exposed via
1912/// `#[cfg(test)]` users).
1913#[cfg(test)]
1914pub fn reset_compdef_state() {
1915    *COMPDEF_STATE.lock().unwrap() = Some(CompdefState::default());
1916}
1917
1918/// Snapshot of session-side state — useful for `compdump` to read
1919/// without locking inside the hot path.
1920pub fn snapshot_compdef_state() -> CompdefState {
1921    with_state(|s| CompdefState {
1922        comps: s.comps.clone(),
1923        services: s.services.clone(),
1924        patcomps: s.patcomps.clone(),
1925        postpatcomps: s.postpatcomps.clone(),
1926        compautos: s.compautos.clone(),
1927    })
1928}
1929
1930#[cfg(test)]
1931mod tests {
1932    use super::*;
1933
1934    #[test]
1935    fn test_parse_compdef_commands() {
1936        let def = parse_first_line("#compdef git svn hg");
1937        match def {
1938            CompFileDef::CompDef(CompDef::Commands(cmds)) => {
1939                assert_eq!(cmds, vec!["git", "svn", "hg"]);
1940            }
1941            _ => panic!("Expected Commands"),
1942        }
1943    }
1944
1945    #[test]
1946    fn test_parse_compdef_pattern() {
1947        let def = parse_first_line("#compdef -p 'c*'");
1948        match def {
1949            CompFileDef::CompDef(CompDef::Pattern(pats)) => {
1950                assert_eq!(pats, vec!["'c*'".to_string()]);
1951            }
1952            _ => panic!("Expected Pattern"),
1953        }
1954    }
1955
1956    // Bug #657 — `#compdef -P pat` must route to `_postpatcomps` (post-pattern),
1957    // NOT `_comps`; `#compdef -p pat` to `_patcomps` only, NOT `_comps`.
1958    #[test]
1959    fn test_parse_compdef_postpattern_routing() {
1960        match parse_first_line("#compdef -P 'pip[0-9.]#'") {
1961            CompFileDef::CompDef(CompDef::PostPattern(pats)) => {
1962                assert_eq!(pats, vec!["'pip[0-9.]#'".to_string()]);
1963            }
1964            other => panic!("Expected PostPattern, got {:?}", other),
1965        }
1966    }
1967
1968    #[test]
1969    fn test_parse_autoload() {
1970        let def = parse_first_line("#autoload -U -z");
1971        match def {
1972            CompFileDef::Autoload(opts) => {
1973                assert_eq!(opts, vec!["-U", "-z"]);
1974            }
1975            _ => panic!("Expected Autoload"),
1976        }
1977    }
1978
1979    #[test]
1980    fn test_parse_compdef_key() {
1981        let def = parse_first_line("#compdef -k complete-word ^X^C");
1982        match def {
1983            CompFileDef::CompDef(CompDef::KeyBinding { style, keys }) => {
1984                assert_eq!(style, "complete-word");
1985                assert_eq!(keys, vec!["^X^C"]);
1986            }
1987            _ => panic!("Expected KeyBinding"),
1988        }
1989    }
1990
1991    #[test]
1992    fn test_parse_compdef_redirect_context() {
1993        // _bzip2 line: has regular commands + context entries with services
1994        let def = parse_first_line("#compdef bzip2 bunzip2 bzcat=bunzip2 bzip2recover -redirect-,<,bunzip2=bunzip2 -redirect-,>,bzip2=bunzip2 -redirect-,<,bzip2=bzip2");
1995        match def {
1996            CompFileDef::CompDef(CompDef::Commands(cmds)) => {
1997                // Should contain all entries
1998                assert!(cmds.contains(&"bzip2".to_string()), "missing bzip2");
1999                assert!(cmds.contains(&"bunzip2".to_string()), "missing bunzip2");
2000                assert!(
2001                    cmds.contains(&"bzcat=bunzip2".to_string()),
2002                    "missing bzcat=bunzip2"
2003                );
2004                assert!(
2005                    cmds.contains(&"bzip2recover".to_string()),
2006                    "missing bzip2recover"
2007                );
2008                assert!(
2009                    cmds.contains(&"-redirect-,<,bunzip2=bunzip2".to_string()),
2010                    "missing redirect bunzip2"
2011                );
2012                assert!(
2013                    cmds.contains(&"-redirect-,>,bzip2=bunzip2".to_string()),
2014                    "missing redirect >,bzip2"
2015                );
2016                assert!(
2017                    cmds.contains(&"-redirect-,<,bzip2=bzip2".to_string()),
2018                    "missing redirect <,bzip2"
2019                );
2020                assert_eq!(cmds.len(), 7, "cmds: {:?}", cmds);
2021            }
2022            other => panic!("Expected Commands, got {:?}", other),
2023        }
2024    }
2025
2026    #[test]
2027    fn test_parse_compdef_context_entries() {
2028        // -default- style entries
2029        let def = parse_first_line("#compdef -default-");
2030        match def {
2031            CompFileDef::CompDef(CompDef::Commands(cmds)) => {
2032                assert_eq!(cmds, vec!["-default-"]);
2033            }
2034            other => panic!("Expected Commands, got {:?}", other),
2035        }
2036
2037        // bare hyphen + commands
2038        let def = parse_first_line("#compdef - nohup eval time");
2039        match def {
2040            CompFileDef::CompDef(CompDef::Commands(cmds)) => {
2041                assert!(cmds.contains(&"-".to_string()));
2042                assert!(cmds.contains(&"nohup".to_string()));
2043                assert!(cmds.contains(&"eval".to_string()));
2044                assert!(cmds.contains(&"time".to_string()));
2045            }
2046            other => panic!("Expected Commands, got {:?}", other),
2047        }
2048
2049        // -value- entries
2050        let def = parse_first_line("#compdef -value- -array-value- -value-,-default-,-default-");
2051        match def {
2052            CompFileDef::CompDef(CompDef::Commands(cmds)) => {
2053                assert!(cmds.contains(&"-value-".to_string()));
2054                assert!(cmds.contains(&"-array-value-".to_string()));
2055                assert!(cmds.contains(&"-value-,-default-,-default-".to_string()));
2056            }
2057            other => panic!("Expected Commands, got {:?}", other),
2058        }
2059    }
2060
2061    #[test]
2062    fn test_is_context_entry() {
2063        assert!(is_context_entry("-default-"));
2064        assert!(is_context_entry("-redirect-"));
2065        assert!(is_context_entry("-value-,DISPLAY,-default-"));
2066        assert!(is_context_entry("-redirect-,<,bunzip2=bunzip2"));
2067        assert!(is_context_entry("-redirect-,>,bzip2"));
2068        assert!(!is_context_entry("-p")); // option flag, not context
2069        assert!(!is_context_entry("-P")); // option flag
2070        assert!(!is_context_entry("git")); // regular command
2071    }
2072
2073    // =================================================================
2074    // compdef() tests — faithful to upstream `Completion/compinit`
2075    // sh:253-446. Lock the global state via reset_compdef_state to
2076    // isolate each case.
2077    // =================================================================
2078
2079    fn run(args: &[&str]) -> i32 {
2080        let owned: Vec<String> = args.iter().map(|s| s.to_string()).collect();
2081        compdef(&owned)
2082    }
2083
2084    #[test]
2085    fn compdef_empty_args_errors() {
2086        let _g = crate::test_util::global_state_lock();
2087        reset_compdef_state();
2088        assert_eq!(compdef(&[]), 1);
2089    }
2090
2091    #[test]
2092    fn compdef_normal_registration() {
2093        // sh:407-419 — `compdef _git git` writes `_comps[git]=_git`.
2094        let _g = crate::test_util::global_state_lock();
2095        reset_compdef_state();
2096        assert_eq!(run(&["_git", "git", "git-commit", "git-push"]), 0);
2097        let state = snapshot_compdef_state();
2098        assert_eq!(state.comps.get("git"), Some(&"_git".to_string()));
2099        assert_eq!(state.comps.get("git-commit"), Some(&"_git".to_string()));
2100        assert_eq!(state.comps.get("git-push"), Some(&"_git".to_string()));
2101    }
2102
2103    #[test]
2104    fn compdef_normal_with_service() {
2105        // sh:408-414 — `cmd=svc` records the service alongside.
2106        let _g = crate::test_util::global_state_lock();
2107        reset_compdef_state();
2108        assert_eq!(run(&["_git", "hub=git"]), 0);
2109        let state = snapshot_compdef_state();
2110        assert_eq!(state.comps.get("hub"), Some(&"_git".to_string()));
2111        assert_eq!(state.services.get("hub"), Some(&"git".to_string()));
2112    }
2113
2114    #[test]
2115    fn compdef_pattern_via_dash_p() {
2116        // sh:393-398 — `-p` writes into `_patcomps`.
2117        let _g = crate::test_util::global_state_lock();
2118        reset_compdef_state();
2119        assert_eq!(run(&["-p", "_test", "*-test"]), 0);
2120        let state = snapshot_compdef_state();
2121        assert_eq!(state.patcomps.get("*-test"), Some(&"_test".to_string()));
2122    }
2123
2124    #[test]
2125    fn compdef_postpattern_via_dash_p_caps() {
2126        // sh:400-405 — `-P` → `_postpatcomps`.
2127        let _g = crate::test_util::global_state_lock();
2128        reset_compdef_state();
2129        assert_eq!(run(&["-P", "_last", "_*"]), 0);
2130        let state = snapshot_compdef_state();
2131        assert_eq!(state.postpatcomps.get("_*"), Some(&"_last".to_string()));
2132    }
2133
2134    #[test]
2135    fn compdef_pattern_with_eq_rewrites_to_eq_form() {
2136        // sh:394-397 — `key=val` form is rewritten to `=val=func`.
2137        let _g = crate::test_util::global_state_lock();
2138        reset_compdef_state();
2139        assert_eq!(run(&["-p", "_test", "*=postfix"]), 0);
2140        let state = snapshot_compdef_state();
2141        assert_eq!(state.patcomps.get("*"), Some(&"=postfix=_test".to_string()));
2142    }
2143
2144    #[test]
2145    fn compdef_delete_removes_from_comps() {
2146        // sh:426-444 — `-d` deletes from the right hash.
2147        let _g = crate::test_util::global_state_lock();
2148        reset_compdef_state();
2149        run(&["_git", "git"]);
2150        assert!(snapshot_compdef_state().comps.contains_key("git"));
2151        assert_eq!(run(&["-d", "git"]), 0);
2152        assert!(!snapshot_compdef_state().comps.contains_key("git"));
2153    }
2154
2155    #[test]
2156    fn compdef_delete_pattern_removes_from_patcomps() {
2157        // sh:429-432 — `-d -p` deletes a pattern entry.
2158        let _g = crate::test_util::global_state_lock();
2159        reset_compdef_state();
2160        run(&["-p", "_test", "*-test"]);
2161        assert!(snapshot_compdef_state().patcomps.contains_key("*-test"));
2162        assert_eq!(run(&["-d", "-p", "*-test"]), 0);
2163        assert!(!snapshot_compdef_state().patcomps.contains_key("*-test"));
2164    }
2165
2166    #[test]
2167    fn compdef_no_clobber_skips_existing() {
2168        // sh:415 — `-n` keeps the existing binding.
2169        let _g = crate::test_util::global_state_lock();
2170        reset_compdef_state();
2171        run(&["_first", "git"]);
2172        run(&["-n", "_second", "git"]);
2173        assert_eq!(
2174            snapshot_compdef_state().comps.get("git"),
2175            Some(&"_first".to_string())
2176        );
2177    }
2178
2179    #[test]
2180    fn compdef_inline_type_switch_dash_p() {
2181        // sh:385-390 — bare `-p` mid-args toggles to pattern mode.
2182        let _g = crate::test_util::global_state_lock();
2183        reset_compdef_state();
2184        run(&["_x", "cmd1", "-p", "pat*", "-N", "cmd2"]);
2185        let s = snapshot_compdef_state();
2186        assert_eq!(s.comps.get("cmd1"), Some(&"_x".to_string()));
2187        assert_eq!(s.patcomps.get("pat*"), Some(&"_x".to_string()));
2188        assert_eq!(s.comps.get("cmd2"), Some(&"_x".to_string()));
2189    }
2190
2191    #[test]
2192    fn compdef_combined_flags_an() {
2193        // sh:267 getopts allows `-an` combined.
2194        let _g = crate::test_util::global_state_lock();
2195        reset_compdef_state();
2196        // `-an` = autol + new
2197        assert_eq!(run(&["-an", "_git", "git"]), 0);
2198        let s = snapshot_compdef_state();
2199        assert_eq!(s.comps.get("git"), Some(&"_git".to_string()));
2200        // -a triggers compautos registration
2201        assert_eq!(s.compautos.get("_git"), Some(&"-rUz".to_string()));
2202    }
2203
2204    #[test]
2205    fn compdef_service_alias_mode_resolves_existing_func() {
2206        // sh:298-326  — first arg with `=` triggers service-alias.
2207        //   Each entry resolves via `_services[(r)$svc]` reverse +
2208        //   `_comps[$svc]`.
2209        let _g = crate::test_util::global_state_lock();
2210        reset_compdef_state();
2211        run(&["_git", "git"]); // first set up git→_git
2212                               // Now `hub=git` should reuse _git
2213        assert_eq!(run(&["hub=git"]), 0);
2214        let s = snapshot_compdef_state();
2215        assert_eq!(s.comps.get("hub"), Some(&"_git".to_string()));
2216        assert_eq!(s.services.get("hub"), Some(&"git".to_string()));
2217    }
2218
2219    #[test]
2220    fn compdef_service_alias_unknown_returns_one() {
2221        // sh:316-318  unknown svc → error
2222        let _g = crate::test_util::global_state_lock();
2223        reset_compdef_state();
2224        assert_eq!(run(&["xyz=never-registered"]), 1);
2225    }
2226
2227    #[test]
2228    fn compdef_unknown_flag_errors() {
2229        let _g = crate::test_util::global_state_lock();
2230        reset_compdef_state();
2231        assert_eq!(run(&["-z", "_x", "cmd"]), 1);
2232    }
2233
2234    #[test]
2235    fn compdef_publishes_state_to_shell_arrays() {
2236        // `_comps` is an ASSOCIATIVE array in zsh (`typeset -gHA`), so the
2237        // shell-side view must be a hash where `${_comps[git]}` == `_git`.
2238        // (Previously published via setaparam as a flat array, which broke
2239        // `${_comps[cmd]}` key lookup and every completion — Bug #655.)
2240        let _g = crate::test_util::global_state_lock();
2241        reset_compdef_state();
2242        run(&["_git", "git"]);
2243        // Must be a proper association, not a flat array.
2244        let map = crate::ported::params::paramtab_hashed_storage()
2245            .lock()
2246            .unwrap()
2247            .get("_comps")
2248            .cloned()
2249            .expect("_comps must be a hashed (associative) param");
2250        assert_eq!(map.get("git").map(String::as_str), Some("_git"));
2251    }
2252}