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 for w in STANDARD_COMPLETE_WIDGETS {
709 // `zle -C <w> .<w> _main_complete` — args are post-flag:
710 // [target-thingy, base-comp-widget, completion-func].
711 let args = [
712 w.to_string(),
713 format!(".{}", w),
714 "_main_complete".to_string(),
715 ];
716 if crate::ported::zle::zle_thingy::bin_zle_complete("zle", &args, &empty_ops, 0) == 0 {
717 count += 1;
718 }
719 }
720 // sh:560 — `zle -C menu-select .menu-select _main_complete` (only
721 // succeeds when the `.menu-select` base widget exists, i.e.
722 // `zsh/complist` is loaded; bin_zle_complete returns 1 otherwise).
723 {
724 let args = [
725 "menu-select".to_string(),
726 ".menu-select".to_string(),
727 "_main_complete".to_string(),
728 ];
729 if crate::ported::zle::zle_thingy::bin_zle_complete("zle", &args, &empty_ops, 0) == 0 {
730 count += 1;
731 }
732 }
733 count
734}
735
736/// sh:564-569 — when the configured `completer` chain includes
737/// `_expand` AND `^i` is currently bound to `expand-or-complete`,
738/// rebind `^i` to `complete-word` so users don't get unexpected
739/// glob expansion on TAB.
740pub fn maybe_rebind_tab_for_expand() {
741 let curcontext = crate::ported::params::getsparam("curcontext").unwrap_or_default();
742 let completers = crate::ported::modules::zutil::lookupstyle(
743 &format!(":completion:{}:", curcontext),
744 "completer",
745 );
746 let has_expand = completers
747 .iter()
748 .any(|c| c == "_expand" || c.starts_with("_expand:"));
749 if !has_expand {
750 return;
751 }
752 // sh:564 — `bindkey '^i' | IFS=$' \t' read -A _i_line`. We can't
753 // capture bindkey output without a real executor, so just
754 // dispatch the rebind unconditionally when _expand is in
755 // the chain. The shell guards on `expand-or-complete` being
756 // the current binding; without capture we'd be over-eager,
757 // so dispatch via the hook (no-op when no executor wired).
758 let _ = crate::ported::exec::dispatch_function_call(
759 "bindkey",
760 &["^i".to_string(), "complete-word".to_string()],
761 );
762}
763
764// `compaudit` lives in its own file (`src/compsys/ported/compaudit.rs`)
765// per zsh upstream's layout (`Completion/compaudit` is a sibling of
766// `Completion/compinit`). Re-export the entry point so existing
767// compinit-facing callers don't need to change.
768pub use super::compaudit::{compaudit, CompauditError};
769
770/// Completion definition from #compdef line
771#[derive(Clone, Debug)]
772pub enum CompDef {
773 /// Regular command completion: #compdef cmd1 cmd2 ...
774 Commands(Vec<String>),
775 /// Pattern completion: #compdef -p 'pattern'
776 Pattern(String),
777 /// Post-pattern completion: #compdef -P 'pattern'
778 PostPattern(String),
779 /// Key binding: #compdef -k style key1 key2 ...
780 KeyBinding { style: String, keys: Vec<String> },
781 /// Widget key binding: #compdef -K widget style key
782 WidgetKey {
783 widget: String,
784 style: String,
785 key: String,
786 },
787}
788
789/// Parsed completion file
790#[derive(Clone, Debug)]
791pub struct CompFile {
792 /// Full path to the file
793 pub path: PathBuf,
794 /// Function name (filename without path)
795 pub name: String,
796 /// What this file defines
797 pub def: CompFileDef,
798 /// Full file body (read during scan for caching)
799 pub body: Option<String>,
800}
801
802/// What a completion file defines
803#[derive(Clone, Debug)]
804pub enum CompFileDef {
805 /// #compdef - completion function
806 CompDef(CompDef),
807 /// #autoload - helper function with options
808 Autoload(Vec<String>),
809 None,
810}
811
812/// Result of compinit scan
813#[derive(Debug, Default)]
814pub struct CompInitResult {
815 /// Command -> function mapping (_comps)
816 pub comps: HashMap<String, String>,
817 /// Command -> service mapping (_services)
818 pub services: HashMap<String, String>,
819 /// Pattern -> function mapping (_patcomps)
820 pub patcomps: HashMap<String, String>,
821 /// Post-pattern -> function mapping (_postpatcomps)
822 pub postpatcomps: HashMap<String, String>,
823 /// Autoload functions with options (_compautos)
824 pub compautos: HashMap<String, String>,
825 /// All scanned files
826 pub files: Vec<CompFile>,
827 /// Scan duration
828 pub scan_time_ms: u64,
829 /// Number of directories scanned
830 pub dirs_scanned: usize,
831 /// Number of files scanned
832 pub files_scanned: usize,
833}
834
835/// Parse the first line of a completion file
836///
837/// Handles all #compdef variants:
838/// - `#compdef cmd1 cmd2` - regular commands
839/// - `#compdef - cmd1 cmd2` - bare hyphen + commands (hyphen maps to '-')
840/// - `#compdef -default-` - special context entries
841/// - `#compdef -value-,VAR,-default-` - value context entries
842/// - `#compdef -p pattern` - pattern completions
843/// - `#compdef -P pattern` - post-pattern completions
844/// - `#compdef -k style key` - key bindings
845/// - `#compdef -K widget style key` - widget key bindings
846fn parse_first_line(line: &str) -> CompFileDef {
847 let line = line.trim();
848
849 if let Some(rest) = line.strip_prefix("#compdef") {
850 let rest = rest.trim();
851 if rest.is_empty() {
852 return CompFileDef::None;
853 }
854
855 let parts: Vec<&str> = rest.split_whitespace().collect();
856 if parts.is_empty() {
857 return CompFileDef::None;
858 }
859
860 // Check for special options first
861 match parts[0] {
862 "-p" if parts.len() >= 2 => {
863 CompFileDef::CompDef(CompDef::Pattern(parts[1].to_string()))
864 }
865 "-P" if parts.len() >= 2 => {
866 // -P patterns go directly into _comps (not _patcomps!)
867 // zsh puts these patterns as keys in _comps hash
868 // Can have multiple: #compdef -P pattern1 -P pattern2 cmd1 cmd2
869 let mut all_cmds = Vec::new();
870 let mut i = 0;
871 while i < parts.len() {
872 if parts[i] == "-P" && i + 1 < parts.len() {
873 // Pattern goes directly as a key in _comps
874 all_cmds.push(parts[i + 1].to_string());
875 i += 2;
876 } else if !parts[i].starts_with('-') || is_context_entry(parts[i]) {
877 all_cmds.push(parts[i].to_string());
878 i += 1;
879 } else {
880 i += 1;
881 }
882 }
883 if all_cmds.is_empty() {
884 CompFileDef::None
885 } else {
886 CompFileDef::CompDef(CompDef::Commands(all_cmds))
887 }
888 }
889 "-k" if parts.len() >= 3 => CompFileDef::CompDef(CompDef::KeyBinding {
890 style: parts[1].to_string(),
891 keys: parts[2..].iter().map(|s| s.to_string()).collect(),
892 }),
893 "-K" if parts.len() >= 4 => CompFileDef::CompDef(CompDef::WidgetKey {
894 widget: parts[1].to_string(),
895 style: parts[2].to_string(),
896 key: parts[3].to_string(),
897 }),
898 _ => {
899 // Parse command definitions, including:
900 // - bare "-" (maps to '-' in _comps)
901 // - context entries like "-default-", "-redirect-", "-value-,VAR,-default-"
902 // - regular commands
903 //
904 // Skip actual option flags like "-n" but keep context entries
905 let cmds: Vec<String> = parts
906 .iter()
907 .filter(|s| {
908 // Keep if:
909 // - bare hyphen "-"
910 // - context entry like "-foo-" or "-value-,X,-default-"
911 // - regular command (no leading hyphen)
912 **s == "-" || is_context_entry(s) || !s.starts_with('-')
913 })
914 .map(|s| s.to_string())
915 .collect();
916 if cmds.is_empty() {
917 CompFileDef::None
918 } else {
919 CompFileDef::CompDef(CompDef::Commands(cmds))
920 }
921 }
922 }
923 } else if let Some(rest) = line.strip_prefix("#autoload") {
924 let opts: Vec<String> = rest.split_whitespace().map(|s| s.to_string()).collect();
925 CompFileDef::Autoload(opts)
926 } else {
927 CompFileDef::None
928 }
929}
930
931/// Check if a string is a zsh completion context entry
932/// Context entries are like: -default-, -redirect-, -command-, -value-,VAR,-default-
933/// Also handles service syntax: -redirect-,<,bunzip2=bunzip2
934fn is_context_entry(s: &str) -> bool {
935 if !s.starts_with('-') {
936 return false;
937 }
938 // Strip service suffix for checking
939 let base = s.split('=').next().unwrap_or(s);
940
941 // Check if it's a known context pattern:
942 // 1. Ends with '-' like -default-, -redirect-
943 // 2. Contains comma (context specifiers like -redirect-,<,bunzip2 or -value-,VAR,-default-)
944 // 3. But NOT single letter options like -p, -P, -k, -K, -n
945 if base.len() <= 2 {
946 return base == "-"; // bare hyphen is a context entry
947 }
948
949 base.ends_with('-') || base.contains(',')
950}
951
952/// Scan a single completion file - reads full body for caching
953fn scan_file(path: &Path) -> Option<CompFile> {
954 let name = path.file_name()?.to_string_lossy().to_string();
955
956 // Must start with underscore
957 if !name.starts_with('_') {
958 return None;
959 }
960
961 // Skip certain patterns
962 if name.contains(';')
963 || name.contains('|')
964 || name.contains('&')
965 || name.ends_with('~')
966 || name.ends_with(".zwc")
967 {
968 return None;
969 }
970
971 // Read entire file at once (will be cached in SQLite)
972 let body = fs::read_to_string(path).ok()?;
973
974 // Parse first line for directive
975 let first_line = body.lines().next().unwrap_or("");
976 let def = parse_first_line(first_line);
977
978 Some(CompFile {
979 path: path.to_path_buf(),
980 name,
981 def,
982 body: Some(body),
983 })
984}
985
986/// Scan a directory for completion files (parallel)
987fn scan_directory(dir: &Path) -> Vec<CompFile> {
988 let entries = match fs::read_dir(dir) {
989 Ok(e) => e,
990 Err(_) => return Vec::new(),
991 };
992
993 let paths: Vec<PathBuf> = entries
994 .filter_map(|e| e.ok())
995 .map(|e| e.path())
996 .filter(|p| p.is_file())
997 .collect();
998
999 // Parallel scan of files within directory
1000 paths.par_iter().filter_map(|p| scan_file(p)).collect()
1001}
1002
1003/// Initialize the completion system by scanning fpath
1004///
1005/// This is the main entry point - replaces the zsh compinit function.
1006/// Uses rayon for parallel directory and file scanning.
1007pub fn compinit(fpath: &[PathBuf]) -> CompInitResult {
1008 let start = Instant::now();
1009
1010 // sh:129-134 — install `$_comp_dumpfile` default so engine code
1011 // that calls `getsparam("_comp_dumpfile")` sees a sensible
1012 // path. User-supplied `compinit -d FILE` overrides.
1013 if crate::ported::params::getsparam("_comp_dumpfile")
1014 .map(|s| s.is_empty())
1015 .unwrap_or(true)
1016 {
1017 let _ = crate::ported::params::setsparam(
1018 "_comp_dumpfile",
1019 &default_dumpfile_path().to_string_lossy(),
1020 );
1021 }
1022
1023 // sh:138-172 — publish `_comp_options` to the shell-side param
1024 // table so `$_comp_setup` eval at every entry point picks it
1025 // up. Use the canonical const list.
1026 crate::ported::params::setaparam(
1027 "_comp_options",
1028 COMP_OPTIONS.iter().map(|s| s.to_string()).collect(),
1029 );
1030
1031 // sh:180-190 — publish `_comp_setup` (the eval string).
1032 let _ = crate::ported::params::setsparam("_comp_setup", COMP_SETUP_EVAL);
1033
1034 // sh:195-197 — initialize the pre/post-hook arrays.
1035 init_comp_funcs_arrays();
1036
1037 // Track seen function names (first one wins)
1038 let seen: Mutex<HashSet<String>> = Mutex::new(HashSet::new());
1039
1040 // Parallel scan of all directories
1041 let all_files: Vec<CompFile> = fpath
1042 .par_iter()
1043 .filter(|dir| dir.as_os_str() != "." && dir.exists())
1044 .flat_map(|dir| scan_directory(dir))
1045 .filter(|f| {
1046 let mut seen = seen.lock().unwrap();
1047 if seen.contains(&f.name) {
1048 false
1049 } else {
1050 seen.insert(f.name.clone());
1051 true
1052 }
1053 })
1054 .collect();
1055
1056 let files_scanned = all_files.len();
1057 let dirs_scanned = fpath.len();
1058
1059 // Build the result maps
1060 let mut result = CompInitResult {
1061 scan_time_ms: start.elapsed().as_millis() as u64,
1062 dirs_scanned,
1063 files_scanned,
1064 ..Default::default()
1065 };
1066
1067 for file in &all_files {
1068 match &file.def {
1069 CompFileDef::CompDef(compdef) => {
1070 match compdef {
1071 CompDef::Commands(cmds) => {
1072 for cmd in cmds {
1073 // Handle service syntax: cmd=service
1074 if let Some(eq_pos) = cmd.find('=') {
1075 let cmd_name = &cmd[..eq_pos];
1076 let service = &cmd[eq_pos + 1..];
1077 result.comps.insert(cmd_name.to_string(), file.name.clone());
1078 result
1079 .services
1080 .insert(cmd_name.to_string(), service.to_string());
1081 } else {
1082 result.comps.insert(cmd.clone(), file.name.clone());
1083 }
1084 }
1085 }
1086 CompDef::Pattern(pat) => {
1087 // -p patterns go to BOTH _comps and _patcomps (zsh behavior)
1088 result.comps.insert(pat.clone(), file.name.clone());
1089 result.patcomps.insert(pat.clone(), file.name.clone());
1090 }
1091 CompDef::PostPattern(pat) => {
1092 result.postpatcomps.insert(pat.clone(), file.name.clone());
1093 }
1094 CompDef::KeyBinding { .. } | CompDef::WidgetKey { .. } => {
1095 // Key bindings need to be handled by the shell
1096 }
1097 }
1098 }
1099 CompFileDef::Autoload(opts) => {
1100 let opts_str = opts.join(" ");
1101 result.compautos.insert(file.name.clone(), opts_str);
1102 }
1103 CompFileDef::None => {}
1104 }
1105 }
1106
1107 result.files = all_files;
1108
1109 // sh:553-569 — the standard completion-widget rebind (`zle -C
1110 // complete-word .complete-word _main_complete` × 8 + conditional
1111 // menu-select + TAB/_expand rebind) is NOT done here. `compinit`
1112 // ships this fpath scan to a worker-pool thread (see
1113 // `ext_builtins::builtin_compinit`), and ZLE keymaps/widgets live
1114 // on the main thread — a `zle -C` issued from a worker never
1115 // reaches the interactive keymap, so TAB stays bound to the
1116 // builtin `expand-or-complete` and `_main_complete` never fires.
1117 // The rebind is instead performed synchronously on the main
1118 // thread in `builtin_compinit`, where it belongs; it needs only
1119 // `_main_complete` (a Rust fn, always present), not the scan
1120 // results, so deferring it to the background is unnecessary.
1121
1122 // Publish the result-side compdef state so downstream `getaparam(
1123 // "_comps")` etc. queries reflect the scan. This is the new-
1124 // compinit-finish complement of `compdef()`'s per-call publish.
1125 with_state(|s| {
1126 for (k, v) in &result.comps {
1127 s.comps.insert(k.clone(), v.clone());
1128 }
1129 for (k, v) in &result.services {
1130 s.services.insert(k.clone(), v.clone());
1131 }
1132 for (k, v) in &result.patcomps {
1133 s.patcomps.insert(k.clone(), v.clone());
1134 }
1135 for (k, v) in &result.postpatcomps {
1136 s.postpatcomps.insert(k.clone(), v.clone());
1137 }
1138 for (k, v) in &result.compautos {
1139 s.compautos.insert(k.clone(), v.clone());
1140 }
1141 publish_compdef_state(s);
1142 });
1143
1144 result
1145}
1146
1147// `compdump`, `check_dump`, and `escape_zsh_string` moved to
1148// `compsys/ported/compdump.rs` (1:1 with upstream `Completion/compdump`).
1149pub use super::compdump::{check_dump, compdump};
1150
1151/// Build SQLite cache from fpath scan
1152///
1153/// This is the main entry point for initializing the completion system.
1154/// It scans fpath directories, parses #compdef directives, and populates
1155/// the SQLite cache for fast lookups.
1156pub fn build_cache_from_fpath(
1157 fpath: &[PathBuf],
1158 cache: &mut crate::compsys::cache::CompsysCache,
1159) -> std::io::Result<CompInitResult> {
1160 use std::time::Instant;
1161
1162 let t0 = Instant::now();
1163 let result = compinit(fpath);
1164 let scan_time = t0.elapsed();
1165
1166 let t1 = Instant::now();
1167
1168 // Populate comps table (_comps hash)
1169 let comps: Vec<(String, String)> = result
1170 .comps
1171 .iter()
1172 .map(|(k, v)| (k.clone(), v.clone()))
1173 .collect();
1174 cache
1175 .set_comps_bulk(&comps)
1176 .map_err(|e| std::io::Error::other(e.to_string()))?;
1177
1178 // Populate services table (_services hash)
1179 let services: Vec<(String, String)> = result
1180 .services
1181 .iter()
1182 .map(|(k, v)| (k.clone(), v.clone()))
1183 .collect();
1184 cache
1185 .set_services_bulk(&services)
1186 .map_err(|e| std::io::Error::other(e.to_string()))?;
1187
1188 // Populate patcomps table (_patcomps hash)
1189 for (pattern, function) in &result.patcomps {
1190 cache
1191 .set_patcomp(pattern, function)
1192 .map_err(|e| std::io::Error::other(e.to_string()))?;
1193 }
1194
1195 // Populate postpatcomps (stored in patcomps with a marker, or separate table if needed)
1196 // For now, we'll merge them into patcomps
1197 for (pattern, function) in &result.postpatcomps {
1198 cache
1199 .set_patcomp(pattern, function)
1200 .map_err(|e| std::io::Error::other(e.to_string()))?;
1201 }
1202
1203 let comps_time = t1.elapsed();
1204 let t2 = Instant::now();
1205
1206 // Populate autoloads table with function bodies for instant loading
1207 // Bodies were already read during parallel scan - no extra I/O here
1208 let autoloads: Vec<(String, String, String)> = result
1209 .files
1210 .iter()
1211 .filter(|f| matches!(f.def, CompFileDef::CompDef(_) | CompFileDef::Autoload(_)))
1212 .filter_map(|f| {
1213 let path_str = f.path.to_string_lossy().to_string();
1214 let body = f.body.as_ref()?.clone();
1215 Some((f.name.clone(), path_str, body))
1216 })
1217 .collect();
1218 cache
1219 .add_autoloads_with_bodies_bulk(&autoloads)
1220 .map_err(|e| std::io::Error::other(e.to_string()))?;
1221
1222 let autoloads_time = t2.elapsed();
1223
1224 // Timing logged by caller in vm_helper via tracing
1225
1226 Ok(result)
1227}
1228
1229/// Load _comps from existing cache (instantaneous)
1230///
1231/// Returns a CompInitResult populated from the SQLite cache without rescanning fpath.
1232/// Use this after the cache has been built with `build_cache_from_fpath`.
1233///
1234/// This is the equivalent of `compinit -C` with a valid zcompdump - it skips
1235/// the fpath scan entirely and just loads from cache.
1236#[allow(clippy::field_reassign_with_default)] // result is mutated across many subsequent statements; struct-literal init not practical
1237pub fn load_from_cache(
1238 cache: &crate::compsys::cache::CompsysCache,
1239) -> std::io::Result<CompInitResult> {
1240 use std::time::Instant;
1241 let start = Instant::now();
1242
1243 let mut result = CompInitResult::default();
1244
1245 // Load comps - single query
1246 result.comps = cache
1247 .get_all_comps()
1248 .map_err(|e| std::io::Error::other(e.to_string()))?;
1249
1250 // Load patcomps - single query
1251 for (pat, func) in cache
1252 .patcomps_kv()
1253 .map_err(|e| std::io::Error::other(e.to_string()))?
1254 {
1255 result.patcomps.insert(pat, func);
1256 }
1257
1258 // Services are loaded on-demand via cache.get_service() - no need to preload
1259 // This matches zsh behavior where $_services is lazily populated
1260
1261 result.scan_time_ms = start.elapsed().as_millis() as u64;
1262 result.files_scanned = result.comps.len();
1263
1264 Ok(result)
1265}
1266
1267/// Fast check if compinit is needed
1268///
1269/// Returns the number of completion entries in cache, or 0 if cache is empty/invalid.
1270/// Use this to decide whether to run full compinit or load_from_cache.
1271pub fn cache_entry_count(cache: &crate::compsys::cache::CompsysCache) -> usize {
1272 cache.comp_count().unwrap_or(0) as usize
1273}
1274
1275/// Lazy compinit - validates cache exists but doesn't load into memory
1276///
1277/// This is the fastest option for shell startup. It just verifies the cache
1278/// is valid and returns immediately. Actual lookups happen via cache.get_comp().
1279///
1280/// Returns (is_valid, entry_count) in microseconds.
1281pub fn compinit_lazy(cache: &crate::compsys::cache::CompsysCache) -> (bool, usize) {
1282 let count = cache.comp_count().unwrap_or(0) as usize;
1283 (count > 0, count)
1284}
1285
1286/// Check if cache is valid and up-to-date
1287///
1288/// Returns true if cache exists and has entries, false if cache needs to be rebuilt.
1289pub fn cache_is_valid(cache: &crate::compsys::cache::CompsysCache) -> bool {
1290 cache.comp_count().unwrap_or(0) > 0
1291}
1292
1293/// Get system fpath from environment or defaults
1294pub fn get_system_fpath() -> Vec<PathBuf> {
1295 // Try FPATH env var first
1296 if let Ok(fpath_str) = std::env::var("FPATH") {
1297 if !fpath_str.is_empty() {
1298 return fpath_str.split(':').map(PathBuf::from).collect();
1299 }
1300 }
1301
1302 // Default paths for common systems
1303 let mut paths = Vec::new();
1304
1305 // macOS Homebrew
1306 for base in &["/opt/homebrew", "/usr/local"] {
1307 paths.push(PathBuf::from(format!("{}/share/zsh/site-functions", base)));
1308 paths.push(PathBuf::from(format!("{}/share/zsh/functions", base)));
1309 }
1310
1311 // System zsh
1312 for version in &["5.9", "5.8", "5.7"] {
1313 paths.push(PathBuf::from(format!(
1314 "/usr/share/zsh/{}/functions",
1315 version
1316 )));
1317 }
1318 paths.push(PathBuf::from("/usr/share/zsh/functions"));
1319 paths.push(PathBuf::from("/usr/share/zsh/site-functions"));
1320
1321 // Zinit/zplugin common paths
1322 if let Ok(home) = std::env::var("HOME") {
1323 paths.push(PathBuf::from(format!("{}/.zinit/completions", home)));
1324 paths.push(PathBuf::from(format!("{}/.zplugin/completions", home)));
1325 paths.push(PathBuf::from(format!(
1326 "{}/.local/share/zsh/site-functions",
1327 home
1328 )));
1329 }
1330
1331 // Filter to existing directories
1332 paths.into_iter().filter(|p| p.exists()).collect()
1333}
1334
1335/// Options for compinit
1336#[derive(Clone, Debug, Default)]
1337pub struct CompInitOpts {
1338 /// Dump file path (-d)
1339 pub dump_file: Option<PathBuf>,
1340 /// Skip dump (-D)
1341 pub no_dump: bool,
1342 /// Skip security check (-C)
1343 pub no_check: bool,
1344 /// Ignore insecure dirs (-i)
1345 pub ignore_insecure: bool,
1346 /// Use insecure dirs (-u)
1347 pub use_insecure: bool,
1348}
1349
1350impl CompInitOpts {
1351 /// Parse compinit arguments
1352 pub fn parse(args: &[String]) -> Self {
1353 let mut opts = Self::default();
1354 let mut i = 0;
1355
1356 while i < args.len() {
1357 match args[i].as_str() {
1358 "-d" if i + 1 < args.len() && !args[i + 1].starts_with('-') => {
1359 opts.dump_file = Some(PathBuf::from(&args[i + 1]));
1360 i += 1;
1361 }
1362 "-D" => opts.no_dump = true,
1363 "-C" => opts.no_check = true,
1364 "-i" => opts.ignore_insecure = true,
1365 "-u" => opts.use_insecure = true,
1366 _ => {}
1367 }
1368 i += 1;
1369 }
1370
1371 opts
1372 }
1373}
1374
1375// =====================================================================
1376// compdef() — runtime registration entry point.
1377//
1378// Upstream defines this inside `Completion/compinit` (sh:253-446); we
1379// mirror that organizational choice. User `.zshrc` lines like:
1380// compdef _git git
1381// compdef -p '*-test' _test
1382// compdef -d obsolete
1383// land here.
1384//
1385// State lives in a session-side `CompdefState` (a Mutex<HashMap>
1386// quintet for `_comps`, `_services`, `_patcomps`, `_postpatcomps`,
1387// `_compautos`). Cluster ports already read these via shell-side
1388// `getaparam("_comps")` etc.; the published-to-paramtab step
1389// happens in `publish_compdef_state` at the bottom of this section.
1390// =====================================================================
1391
1392/// Session-side compdef registrations. Mirrors the five upstream
1393/// assoc arrays one-for-one.
1394#[derive(Default)]
1395pub struct CompdefState {
1396 pub comps: HashMap<String, String>,
1397 pub services: HashMap<String, String>,
1398 pub patcomps: HashMap<String, String>,
1399 pub postpatcomps: HashMap<String, String>,
1400 pub compautos: HashMap<String, String>,
1401}
1402
1403static COMPDEF_STATE: Mutex<Option<CompdefState>> = Mutex::new(None);
1404
1405/// Lock the session-side state, initializing on first call.
1406fn with_state<F, R>(f: F) -> R
1407where
1408 F: FnOnce(&mut CompdefState) -> R,
1409{
1410 let mut guard = COMPDEF_STATE.lock().unwrap();
1411 if guard.is_none() {
1412 *guard = Some(CompdefState::default());
1413 }
1414 f(guard.as_mut().unwrap())
1415}
1416
1417/// Helper to publish state inside a `with_state` closure (takes
1418/// `&mut` to fit the closure signature).
1419fn publish_compdef_state_mut(s: &mut CompdefState) {
1420 publish_compdef_state(s);
1421}
1422
1423/// Publish the in-memory state back to shell-side assoc arrays so
1424/// engine cluster code (which reads via `getaparam("_comps")` etc.)
1425/// sees the updates. Flat key/value layout per the existing
1426/// per-engine-port convention.
1427fn publish_compdef_state(s: &CompdefState) {
1428 let mut flatten = |arr: &HashMap<String, String>| -> Vec<String> {
1429 let mut sorted: Vec<(&String, &String)> = arr.iter().collect();
1430 sorted.sort_by(|a, b| a.0.cmp(b.0));
1431 let mut out = Vec::with_capacity(sorted.len() * 2);
1432 for (k, v) in sorted {
1433 out.push(k.clone());
1434 out.push(v.clone());
1435 }
1436 out
1437 };
1438 crate::ported::params::setaparam("_comps", flatten(&s.comps));
1439 crate::ported::params::setaparam("_services", flatten(&s.services));
1440 crate::ported::params::setaparam("_patcomps", flatten(&s.patcomps));
1441 crate::ported::params::setaparam("_postpatcomps", flatten(&s.postpatcomps));
1442 crate::ported::params::setaparam("_compautos", flatten(&s.compautos));
1443}
1444
1445/// Parse `compdef`'s short-option flags via the upstream
1446/// `getopts "anpPkKde"` (sh:267).
1447#[derive(Default, Debug)]
1448struct CompdefFlags {
1449 autol: bool,
1450 new: bool,
1451 delete: bool,
1452 eval: bool,
1453 /// Mutually-exclusive `-p`/`-P`/`-k`/`-K`. Only the most-recent
1454 /// wins; upstream errors on duplicate, we tolerate.
1455 spec_type: SpecType,
1456}
1457
1458#[derive(Default, Debug, PartialEq, Clone, Copy)]
1459enum SpecType {
1460 #[default]
1461 Normal,
1462 Pattern,
1463 PostPattern,
1464 Key,
1465 WidgetKey,
1466}
1467
1468fn parse_compdef_flags(args: &[String]) -> Result<(CompdefFlags, usize), String> {
1469 let mut flags = CompdefFlags::default();
1470 let mut idx = 0usize;
1471 while idx < args.len() {
1472 let a = &args[idx];
1473 if !a.starts_with('-') || a == "-" || a == "--" {
1474 break;
1475 }
1476 // sh:267 getopts allows combined flags like `-an`. Walk each
1477 // letter after the leading `-`.
1478 for c in a.chars().skip(1) {
1479 match c {
1480 'a' => flags.autol = true,
1481 'n' => flags.new = true,
1482 'd' => flags.delete = true,
1483 'e' => flags.eval = true,
1484 'p' => flags.spec_type = SpecType::Pattern,
1485 'P' => flags.spec_type = SpecType::PostPattern,
1486 'k' => flags.spec_type = SpecType::Key,
1487 'K' => flags.spec_type = SpecType::WidgetKey,
1488 _ => return Err(format!("compdef: unknown option: -{}", c)),
1489 }
1490 }
1491 idx += 1;
1492 }
1493 Ok((flags, idx))
1494}
1495
1496/// `compdef` — register or unregister completion functions for
1497/// commands. Faithful to upstream `Completion/compinit` sh:253-446.
1498///
1499/// Returns the upstream-compatible exit code: 0 on success, 1 on
1500/// usage error.
1501pub fn compdef(args: &[String]) -> i32 {
1502 // sh:262
1503 if args.is_empty() {
1504 eprintln!("compdef: I need arguments");
1505 return 1;
1506 }
1507 let (flags, mut idx) = match parse_compdef_flags(args) {
1508 Ok(p) => p,
1509 Err(e) => {
1510 eprintln!("{}", e);
1511 return 1;
1512 }
1513 };
1514 // sh:293
1515 if idx >= args.len() {
1516 eprintln!("compdef: I need arguments");
1517 return 1;
1518 }
1519
1520 if flags.delete {
1521 // sh:426-444 -d: delete by name from the right hash
1522 let names = &args[idx..];
1523 with_state(|s| match flags.spec_type {
1524 SpecType::Pattern => {
1525 for n in names {
1526 s.patcomps.remove(n);
1527 }
1528 }
1529 SpecType::PostPattern => {
1530 for n in names {
1531 s.postpatcomps.remove(n);
1532 }
1533 }
1534 SpecType::Key | SpecType::WidgetKey => {
1535 eprintln!("compdef: cannot restore key bindings");
1536 }
1537 SpecType::Normal => {
1538 for n in names {
1539 s.comps.remove(n);
1540 s.services.remove(n);
1541 }
1542 }
1543 });
1544 with_state(publish_compdef_state_mut);
1545 return 0;
1546 }
1547
1548 // sh:298-327 service-alias mode: no flags + first arg contains `=`.
1549 // Each subsequent arg must also contain `=` (else it's an error).
1550 if !flags.eval && args[idx].contains('=') {
1551 let mut ret: i32 = 0;
1552 while idx < args.len() {
1553 let entry = args[idx].clone();
1554 idx += 1;
1555 if !entry.contains('=') {
1556 eprintln!("compdef: invalid argument: {}", entry);
1557 ret = 1;
1558 continue;
1559 }
1560 let mut sp = entry.splitn(2, '=');
1561 let cmd = sp.next().unwrap_or("").to_string();
1562 let svc_in = sp.next().unwrap_or("").to_string();
1563 // sh:307-311 — resolve `$svc` via `_services[(r)$svc]` reverse
1564 // lookup (find an entry mapping TO $svc); else use $svc
1565 // directly. Then look up `_comps[$svc]` for the function.
1566 let resolved_svc = {
1567 with_state(|s| {
1568 s.services
1569 .iter()
1570 .find(|(_, v)| **v == svc_in)
1571 .map(|(k, _)| k.clone())
1572 .unwrap_or(svc_in.clone())
1573 })
1574 };
1575 let func = with_state(|s| {
1576 s.comps
1577 .get(&resolved_svc)
1578 .cloned()
1579 .or_else(|| {
1580 // sh:311 fallback to first matching pat/postpat key
1581 s.patcomps
1582 .iter()
1583 .find(|(k, _)| pattern_matches(k, &svc_in))
1584 .map(|(_, v)| v.clone())
1585 .or_else(|| {
1586 s.postpatcomps
1587 .iter()
1588 .find(|(k, _)| pattern_matches(k, &svc_in))
1589 .map(|(_, v)| v.clone())
1590 })
1591 })
1592 .unwrap_or_default()
1593 });
1594 if func.is_empty() {
1595 eprintln!("compdef: unknown command or service: {}", svc_in);
1596 ret = 1;
1597 continue;
1598 }
1599 let svc_for_state =
1600 with_state(|s| s.services.get(&svc_in).cloned().unwrap_or(svc_in.clone()));
1601 with_state(|s| {
1602 s.comps.insert(cmd.clone(), func.clone());
1603 s.services.insert(cmd, svc_for_state);
1604 });
1605 }
1606 with_state(publish_compdef_state_mut);
1607 return ret;
1608 }
1609
1610 // sh:332-334 First positional after flags is the function name.
1611 let func = args[idx].clone();
1612 idx += 1;
1613
1614 // sh:333 `-a` → autoload
1615 if flags.autol && func.starts_with('_') {
1616 // dispatch `autoload -rUz <func>` via the exec accessors bridge.
1617 let _ = crate::ported::exec::dispatch_function_call(
1618 "autoload",
1619 &["-rUz".to_string(), func.clone()],
1620 );
1621 // Track for the dump file
1622 with_state(|s| {
1623 s.compautos.insert(func.clone(), "-rUz".to_string());
1624 });
1625 }
1626
1627 // sh:336-425
1628 match flags.spec_type {
1629 SpecType::WidgetKey => {
1630 // sh:337-355 -K widget-name comp-widget key (in triples)
1631 let mut i = idx;
1632 while i + 2 < args.len() {
1633 let mut wname = args[i].clone();
1634 let mut comp_widget = args[i + 1].clone();
1635 let key = args[i + 2].clone();
1636 if !wname.starts_with('_') {
1637 wname = format!("_{}", wname);
1638 }
1639 if !comp_widget.starts_with('.') {
1640 comp_widget = format!(".{}", comp_widget);
1641 }
1642 // sh:346 zle -C <wname> <comp_widget> <func>
1643 let _ = crate::ported::exec::dispatch_function_call(
1644 "zle",
1645 &["-C".to_string(), wname.clone(), comp_widget, func.clone()],
1646 );
1647 // sh:347-352 bindkey
1648 let _ = crate::ported::exec::dispatch_function_call("bindkey", &[key, wname]);
1649 i += 3;
1650 }
1651 }
1652 SpecType::Key => {
1653 // sh:356-379 -k style key... (in 1+ pairs)
1654 if idx >= args.len() {
1655 eprintln!("compdef: missing keys");
1656 return 1;
1657 }
1658 let mut style = args[idx].clone();
1659 idx += 1;
1660 if !style.starts_with('.') {
1661 style = format!(".{}", style);
1662 }
1663 // sh:365 zle -C <func> <style> <func>
1664 let _ = crate::ported::exec::dispatch_function_call(
1665 "zle",
1666 &["-C".to_string(), func.clone(), style, func.clone()],
1667 );
1668 for key in &args[idx..] {
1669 let _ = crate::ported::exec::dispatch_function_call(
1670 "bindkey",
1671 &[key.clone(), func.clone()],
1672 );
1673 }
1674 }
1675 _ => {
1676 // sh:381-424 normal / pattern / postpattern
1677 let mut effective_type = flags.spec_type;
1678 while idx < args.len() {
1679 let arg = args[idx].clone();
1680 idx += 1;
1681 // sh:385-390 inline type switch
1682 match arg.as_str() {
1683 "-N" => {
1684 effective_type = SpecType::Normal;
1685 continue;
1686 }
1687 "-p" => {
1688 effective_type = SpecType::Pattern;
1689 continue;
1690 }
1691 "-P" => {
1692 effective_type = SpecType::PostPattern;
1693 continue;
1694 }
1695 _ => {}
1696 }
1697 with_state(|s| match effective_type {
1698 SpecType::Pattern => {
1699 // sh:393-398 — `key=val` rewrites to `=val=func`
1700 if let Some(eq) = arg.find('=') {
1701 let key = arg[..eq].to_string();
1702 let val = arg[eq + 1..].to_string();
1703 s.patcomps.insert(key, format!("={}={}", val, func));
1704 } else {
1705 s.patcomps.insert(arg.clone(), func.clone());
1706 }
1707 }
1708 SpecType::PostPattern => {
1709 if let Some(eq) = arg.find('=') {
1710 let key = arg[..eq].to_string();
1711 let val = arg[eq + 1..].to_string();
1712 s.postpatcomps.insert(key, format!("={}={}", val, func));
1713 } else {
1714 s.postpatcomps.insert(arg.clone(), func.clone());
1715 }
1716 }
1717 _ => {
1718 // sh:407-419 normal: cmd or cmd=svc
1719 let (cmd, svc) = if let Some(eq) = arg.find('=') {
1720 (arg[..eq].to_string(), Some(arg[eq + 1..].to_string()))
1721 } else {
1722 (arg.clone(), None)
1723 };
1724 // sh:415 -n: no-clobber
1725 if flags.new && s.comps.contains_key(&cmd) {
1726 return;
1727 }
1728 s.comps.insert(cmd.clone(), func.clone());
1729 if let Some(svc) = svc {
1730 s.services.insert(cmd, svc);
1731 }
1732 }
1733 });
1734 }
1735 }
1736 }
1737 with_state(publish_compdef_state_mut);
1738 0
1739}
1740
1741/// sh:311 pattern-matching helper. Uses the real `pattern.rs`
1742/// matcher so `(K)` assoc-key glob matching is faithful.
1743fn pattern_matches(pat: &str, s: &str) -> bool {
1744 match crate::ported::pattern::patcompile(
1745 &{
1746 let mut __pat_tok = (pat).to_string();
1747 crate::ported::glob::tokenize(&mut __pat_tok);
1748 __pat_tok
1749 },
1750 0,
1751 None,
1752 ) {
1753 Some(prog) => crate::ported::pattern::pattry(&prog, s),
1754 None => pat == s,
1755 }
1756}
1757
1758/// Reset session-side state (test-only helper; exposed via
1759/// `#[cfg(test)]` users).
1760#[cfg(test)]
1761pub fn reset_compdef_state() {
1762 *COMPDEF_STATE.lock().unwrap() = Some(CompdefState::default());
1763}
1764
1765/// Snapshot of session-side state — useful for `compdump` to read
1766/// without locking inside the hot path.
1767pub fn snapshot_compdef_state() -> CompdefState {
1768 with_state(|s| CompdefState {
1769 comps: s.comps.clone(),
1770 services: s.services.clone(),
1771 patcomps: s.patcomps.clone(),
1772 postpatcomps: s.postpatcomps.clone(),
1773 compautos: s.compautos.clone(),
1774 })
1775}
1776
1777#[cfg(test)]
1778mod tests {
1779 use super::*;
1780
1781 #[test]
1782 fn test_parse_compdef_commands() {
1783 let def = parse_first_line("#compdef git svn hg");
1784 match def {
1785 CompFileDef::CompDef(CompDef::Commands(cmds)) => {
1786 assert_eq!(cmds, vec!["git", "svn", "hg"]);
1787 }
1788 _ => panic!("Expected Commands"),
1789 }
1790 }
1791
1792 #[test]
1793 fn test_parse_compdef_pattern() {
1794 let def = parse_first_line("#compdef -p 'c*'");
1795 match def {
1796 CompFileDef::CompDef(CompDef::Pattern(pat)) => {
1797 assert_eq!(pat, "'c*'");
1798 }
1799 _ => panic!("Expected Pattern"),
1800 }
1801 }
1802
1803 #[test]
1804 fn test_parse_autoload() {
1805 let def = parse_first_line("#autoload -U -z");
1806 match def {
1807 CompFileDef::Autoload(opts) => {
1808 assert_eq!(opts, vec!["-U", "-z"]);
1809 }
1810 _ => panic!("Expected Autoload"),
1811 }
1812 }
1813
1814 #[test]
1815 fn test_parse_compdef_key() {
1816 let def = parse_first_line("#compdef -k complete-word ^X^C");
1817 match def {
1818 CompFileDef::CompDef(CompDef::KeyBinding { style, keys }) => {
1819 assert_eq!(style, "complete-word");
1820 assert_eq!(keys, vec!["^X^C"]);
1821 }
1822 _ => panic!("Expected KeyBinding"),
1823 }
1824 }
1825
1826 #[test]
1827 fn test_parse_compdef_redirect_context() {
1828 // _bzip2 line: has regular commands + context entries with services
1829 let def = parse_first_line("#compdef bzip2 bunzip2 bzcat=bunzip2 bzip2recover -redirect-,<,bunzip2=bunzip2 -redirect-,>,bzip2=bunzip2 -redirect-,<,bzip2=bzip2");
1830 match def {
1831 CompFileDef::CompDef(CompDef::Commands(cmds)) => {
1832 // Should contain all entries
1833 assert!(cmds.contains(&"bzip2".to_string()), "missing bzip2");
1834 assert!(cmds.contains(&"bunzip2".to_string()), "missing bunzip2");
1835 assert!(
1836 cmds.contains(&"bzcat=bunzip2".to_string()),
1837 "missing bzcat=bunzip2"
1838 );
1839 assert!(
1840 cmds.contains(&"bzip2recover".to_string()),
1841 "missing bzip2recover"
1842 );
1843 assert!(
1844 cmds.contains(&"-redirect-,<,bunzip2=bunzip2".to_string()),
1845 "missing redirect bunzip2"
1846 );
1847 assert!(
1848 cmds.contains(&"-redirect-,>,bzip2=bunzip2".to_string()),
1849 "missing redirect >,bzip2"
1850 );
1851 assert!(
1852 cmds.contains(&"-redirect-,<,bzip2=bzip2".to_string()),
1853 "missing redirect <,bzip2"
1854 );
1855 assert_eq!(cmds.len(), 7, "cmds: {:?}", cmds);
1856 }
1857 other => panic!("Expected Commands, got {:?}", other),
1858 }
1859 }
1860
1861 #[test]
1862 fn test_parse_compdef_context_entries() {
1863 // -default- style entries
1864 let def = parse_first_line("#compdef -default-");
1865 match def {
1866 CompFileDef::CompDef(CompDef::Commands(cmds)) => {
1867 assert_eq!(cmds, vec!["-default-"]);
1868 }
1869 other => panic!("Expected Commands, got {:?}", other),
1870 }
1871
1872 // bare hyphen + commands
1873 let def = parse_first_line("#compdef - nohup eval time");
1874 match def {
1875 CompFileDef::CompDef(CompDef::Commands(cmds)) => {
1876 assert!(cmds.contains(&"-".to_string()));
1877 assert!(cmds.contains(&"nohup".to_string()));
1878 assert!(cmds.contains(&"eval".to_string()));
1879 assert!(cmds.contains(&"time".to_string()));
1880 }
1881 other => panic!("Expected Commands, got {:?}", other),
1882 }
1883
1884 // -value- entries
1885 let def = parse_first_line("#compdef -value- -array-value- -value-,-default-,-default-");
1886 match def {
1887 CompFileDef::CompDef(CompDef::Commands(cmds)) => {
1888 assert!(cmds.contains(&"-value-".to_string()));
1889 assert!(cmds.contains(&"-array-value-".to_string()));
1890 assert!(cmds.contains(&"-value-,-default-,-default-".to_string()));
1891 }
1892 other => panic!("Expected Commands, got {:?}", other),
1893 }
1894 }
1895
1896 #[test]
1897 fn test_is_context_entry() {
1898 assert!(is_context_entry("-default-"));
1899 assert!(is_context_entry("-redirect-"));
1900 assert!(is_context_entry("-value-,DISPLAY,-default-"));
1901 assert!(is_context_entry("-redirect-,<,bunzip2=bunzip2"));
1902 assert!(is_context_entry("-redirect-,>,bzip2"));
1903 assert!(!is_context_entry("-p")); // option flag, not context
1904 assert!(!is_context_entry("-P")); // option flag
1905 assert!(!is_context_entry("git")); // regular command
1906 }
1907
1908 // =================================================================
1909 // compdef() tests — faithful to upstream `Completion/compinit`
1910 // sh:253-446. Lock the global state via reset_compdef_state to
1911 // isolate each case.
1912 // =================================================================
1913
1914 fn run(args: &[&str]) -> i32 {
1915 let owned: Vec<String> = args.iter().map(|s| s.to_string()).collect();
1916 compdef(&owned)
1917 }
1918
1919 #[test]
1920 fn compdef_empty_args_errors() {
1921 let _g = crate::test_util::global_state_lock();
1922 reset_compdef_state();
1923 assert_eq!(compdef(&[]), 1);
1924 }
1925
1926 #[test]
1927 fn compdef_normal_registration() {
1928 // sh:407-419 — `compdef _git git` writes `_comps[git]=_git`.
1929 let _g = crate::test_util::global_state_lock();
1930 reset_compdef_state();
1931 assert_eq!(run(&["_git", "git", "git-commit", "git-push"]), 0);
1932 let state = snapshot_compdef_state();
1933 assert_eq!(state.comps.get("git"), Some(&"_git".to_string()));
1934 assert_eq!(state.comps.get("git-commit"), Some(&"_git".to_string()));
1935 assert_eq!(state.comps.get("git-push"), Some(&"_git".to_string()));
1936 }
1937
1938 #[test]
1939 fn compdef_normal_with_service() {
1940 // sh:408-414 — `cmd=svc` records the service alongside.
1941 let _g = crate::test_util::global_state_lock();
1942 reset_compdef_state();
1943 assert_eq!(run(&["_git", "hub=git"]), 0);
1944 let state = snapshot_compdef_state();
1945 assert_eq!(state.comps.get("hub"), Some(&"_git".to_string()));
1946 assert_eq!(state.services.get("hub"), Some(&"git".to_string()));
1947 }
1948
1949 #[test]
1950 fn compdef_pattern_via_dash_p() {
1951 // sh:393-398 — `-p` writes into `_patcomps`.
1952 let _g = crate::test_util::global_state_lock();
1953 reset_compdef_state();
1954 assert_eq!(run(&["-p", "_test", "*-test"]), 0);
1955 let state = snapshot_compdef_state();
1956 assert_eq!(state.patcomps.get("*-test"), Some(&"_test".to_string()));
1957 }
1958
1959 #[test]
1960 fn compdef_postpattern_via_dash_p_caps() {
1961 // sh:400-405 — `-P` → `_postpatcomps`.
1962 let _g = crate::test_util::global_state_lock();
1963 reset_compdef_state();
1964 assert_eq!(run(&["-P", "_last", "_*"]), 0);
1965 let state = snapshot_compdef_state();
1966 assert_eq!(state.postpatcomps.get("_*"), Some(&"_last".to_string()));
1967 }
1968
1969 #[test]
1970 fn compdef_pattern_with_eq_rewrites_to_eq_form() {
1971 // sh:394-397 — `key=val` form is rewritten to `=val=func`.
1972 let _g = crate::test_util::global_state_lock();
1973 reset_compdef_state();
1974 assert_eq!(run(&["-p", "_test", "*=postfix"]), 0);
1975 let state = snapshot_compdef_state();
1976 assert_eq!(state.patcomps.get("*"), Some(&"=postfix=_test".to_string()));
1977 }
1978
1979 #[test]
1980 fn compdef_delete_removes_from_comps() {
1981 // sh:426-444 — `-d` deletes from the right hash.
1982 let _g = crate::test_util::global_state_lock();
1983 reset_compdef_state();
1984 run(&["_git", "git"]);
1985 assert!(snapshot_compdef_state().comps.contains_key("git"));
1986 assert_eq!(run(&["-d", "git"]), 0);
1987 assert!(!snapshot_compdef_state().comps.contains_key("git"));
1988 }
1989
1990 #[test]
1991 fn compdef_delete_pattern_removes_from_patcomps() {
1992 // sh:429-432 — `-d -p` deletes a pattern entry.
1993 let _g = crate::test_util::global_state_lock();
1994 reset_compdef_state();
1995 run(&["-p", "_test", "*-test"]);
1996 assert!(snapshot_compdef_state().patcomps.contains_key("*-test"));
1997 assert_eq!(run(&["-d", "-p", "*-test"]), 0);
1998 assert!(!snapshot_compdef_state().patcomps.contains_key("*-test"));
1999 }
2000
2001 #[test]
2002 fn compdef_no_clobber_skips_existing() {
2003 // sh:415 — `-n` keeps the existing binding.
2004 let _g = crate::test_util::global_state_lock();
2005 reset_compdef_state();
2006 run(&["_first", "git"]);
2007 run(&["-n", "_second", "git"]);
2008 assert_eq!(
2009 snapshot_compdef_state().comps.get("git"),
2010 Some(&"_first".to_string())
2011 );
2012 }
2013
2014 #[test]
2015 fn compdef_inline_type_switch_dash_p() {
2016 // sh:385-390 — bare `-p` mid-args toggles to pattern mode.
2017 let _g = crate::test_util::global_state_lock();
2018 reset_compdef_state();
2019 run(&["_x", "cmd1", "-p", "pat*", "-N", "cmd2"]);
2020 let s = snapshot_compdef_state();
2021 assert_eq!(s.comps.get("cmd1"), Some(&"_x".to_string()));
2022 assert_eq!(s.patcomps.get("pat*"), Some(&"_x".to_string()));
2023 assert_eq!(s.comps.get("cmd2"), Some(&"_x".to_string()));
2024 }
2025
2026 #[test]
2027 fn compdef_combined_flags_an() {
2028 // sh:267 getopts allows `-an` combined.
2029 let _g = crate::test_util::global_state_lock();
2030 reset_compdef_state();
2031 // `-an` = autol + new
2032 assert_eq!(run(&["-an", "_git", "git"]), 0);
2033 let s = snapshot_compdef_state();
2034 assert_eq!(s.comps.get("git"), Some(&"_git".to_string()));
2035 // -a triggers compautos registration
2036 assert_eq!(s.compautos.get("_git"), Some(&"-rUz".to_string()));
2037 }
2038
2039 #[test]
2040 fn compdef_service_alias_mode_resolves_existing_func() {
2041 // sh:298-326 — first arg with `=` triggers service-alias.
2042 // Each entry resolves via `_services[(r)$svc]` reverse +
2043 // `_comps[$svc]`.
2044 let _g = crate::test_util::global_state_lock();
2045 reset_compdef_state();
2046 run(&["_git", "git"]); // first set up git→_git
2047 // Now `hub=git` should reuse _git
2048 assert_eq!(run(&["hub=git"]), 0);
2049 let s = snapshot_compdef_state();
2050 assert_eq!(s.comps.get("hub"), Some(&"_git".to_string()));
2051 assert_eq!(s.services.get("hub"), Some(&"git".to_string()));
2052 }
2053
2054 #[test]
2055 fn compdef_service_alias_unknown_returns_one() {
2056 // sh:316-318 unknown svc → error
2057 let _g = crate::test_util::global_state_lock();
2058 reset_compdef_state();
2059 assert_eq!(run(&["xyz=never-registered"]), 1);
2060 }
2061
2062 #[test]
2063 fn compdef_unknown_flag_errors() {
2064 let _g = crate::test_util::global_state_lock();
2065 reset_compdef_state();
2066 assert_eq!(run(&["-z", "_x", "cmd"]), 1);
2067 }
2068
2069 #[test]
2070 fn compdef_publishes_state_to_shell_arrays() {
2071 // The shell-side `getaparam("_comps")` must see the
2072 // registered entries flat key/value.
2073 let _g = crate::test_util::global_state_lock();
2074 reset_compdef_state();
2075 run(&["_git", "git"]);
2076 let arr = crate::ported::params::getaparam("_comps").unwrap_or_default();
2077 // Flat layout: [k, v, k, v, ...] sorted by key.
2078 assert!(arr.windows(2).any(|w| w[0] == "git" && w[1] == "_git"));
2079 }
2080}