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.
638// sh:180-190 — the value is a single-quoted literal spanning eleven
639// lines, so the 13-space indentation of every continuation line is part
640// of the STRING, not just the source layout. `eval` ignores it, but
641// `$_comp_setup` is readable and was the one parameter whose value still
642// differed from zsh's inside a completion.
643pub const COMP_SETUP_EVAL: &str = concat!(
644 "local -A _comp_caller_options;\n",
645 " _comp_caller_options=(${(kv)options[@]});\n",
646 " setopt localoptions localtraps localpatterns ${_comp_options[@]};\n",
647 " local IFS=$' \\t\\r\\n\\0';\n",
648 " builtin enable -p \\| \\~ \\( \\? \\* \\[ \\< \\^ \\# 2>&-;\n",
649 " exec </dev/null;\n",
650 " trap - ZERR;\n",
651 " local -a reply;\n",
652 " local REPLY;\n",
653 " local REPORTTIME;\n",
654 " unset REPORTTIME"
655);
656
657/// sh:558 — the 8 standard ZLE widgets that compinit rebinds to
658/// `_main_complete` so any of them triggers a completion attempt.
659pub const STANDARD_COMPLETE_WIDGETS: &[&str] = &[
660 "complete-word",
661 "delete-char-or-list",
662 "expand-or-complete",
663 "expand-or-complete-prefix",
664 "list-choices",
665 "menu-complete",
666 "menu-expand-or-complete",
667 "reverse-menu-complete",
668];
669
670// =====================================================================
671// State publication helpers — keep the shell-side `$compprefuncs`
672// and `$comppostfuncs` arrays initialized empty per sh:195-197.
673// =====================================================================
674
675/// Initialize the shell-side `compprefuncs` / `comppostfuncs` arrays
676/// to empty (sh:195-197). Idempotent; safe to call from compinit
677/// before any user-side `_call_function` would have populated them.
678pub fn init_comp_funcs_arrays() {
679 crate::ported::params::setaparam("compprefuncs", Vec::new());
680 crate::ported::params::setaparam("comppostfuncs", Vec::new());
681}
682
683/// `typeset -g[H][A|a] NAME` — declare a global parameter with the
684/// given type/attribute bits WITHOUT disturbing an existing value.
685///
686/// Port of the `bin_typeset` path upstream's declarations take
687/// (`Src/builtin.c:2469-2575`): when the name is absent the parameter
688/// is created with the requested type; when it is already present only
689/// the attribute bits are OR'd in. `compinit`'s `typeset -gHA _comps`
690/// on a re-`compinit` must not empty the table it just loaded, which is
691/// exactly the "already present" arm.
692fn declare_global(name: &str, kind: u32, attrs: u32) {
693 use crate::ported::params::{paramtab, setaparam, sethparam, setsparam};
694 use crate::ported::zsh_h::{PM_ARRAY, PM_HASHED};
695
696 let exists = paramtab()
697 .read()
698 .ok()
699 .map(|t| t.contains_key(name))
700 .unwrap_or(false);
701 if !exists {
702 // Create through the canonical typed setters, NOT a bare
703 // `createparam`: for a hashed parameter the values live in
704 // `paramtab_hashed_storage`, and only `sethparam` allocates
705 // that side. A raw `createparam(PM_HASHED)` produced a
706 // `_comps` that existed but could never be filled — `${#_comps}`
707 // stayed 0 and `_dispatch` found no completer for ANY command,
708 // so every `<cmd> <TAB>` silently did nothing.
709 if kind & PM_HASHED != 0 {
710 sethparam(name, Vec::new());
711 } else if kind & PM_ARRAY != 0 {
712 setaparam(name, Vec::new());
713 } else {
714 let _ = setsparam(name, "");
715 }
716 }
717 // c:Src/builtin.c:2575 — attribute-only update on an existing
718 // parameter; the value is left alone, which is what a re-`compinit`
719 // needs (`typeset -gHA _comps` must not empty a loaded table).
720 if let Ok(mut tab) = paramtab().write() {
721 if let Some(pm) = tab.get_mut(name) {
722 pm.node.flags |= attrs as i32;
723 }
724 }
725}
726
727/// compinit sh:116-197 — the global parameters `compinit` itself
728/// declares, before any of its branches run.
729///
730/// ```text
731/// sh:116 typeset -gHA _comps _services _patcomps _postpatcomps
732/// sh:121 typeset -gHA _compautos
733/// sh:126 typeset -gHA _lastcomp
734/// sh:131 typeset -g _comp_dumpfile="$_i_dumpfile" (-d FILE)
735/// sh:133 typeset -g _comp_dumpfile="${ZDOTDIR:-$HOME}/.zcompdump"
736/// sh:138 typeset -gHa _comp_options
737/// sh:180 typeset -gH _comp_setup='…'
738/// sh:195 typeset -ga compprefuncs comppostfuncs
739/// ```
740///
741/// These are unconditional lines in `compinit`'s body — they run on
742/// every path, dump-hit and fresh-scan alike. zshrs keeps the completer
743/// tables in Rust and only published `_comps`/`_services`/`_patcomps`
744/// on the `-C` cache-hit path, so a real session was missing eight
745/// names that upstream always has, and the four assocs it did publish
746/// carried no `-H` (`PM_HIDEVAL`) bit. That is directly observable:
747/// `unset <TAB>` runs `_vars` → `_parameters`, which offers every
748/// parameter whose `${(t)}` lacks `local`, so the missing declarations
749/// were missing completions.
750pub fn declare_compinit_globals(dumpfile: Option<&str>) {
751 use crate::ported::zsh_h::{PM_ARRAY, PM_HASHED, PM_HIDEVAL, PM_UNIQUE};
752
753 // sh:116 / sh:121 / sh:126 — `typeset -gHA …`.
754 for name in [
755 "_comps",
756 "_services",
757 "_patcomps",
758 "_postpatcomps",
759 "_compautos",
760 "_lastcomp",
761 ] {
762 declare_global(name, PM_HASHED, PM_HIDEVAL);
763 }
764
765 // sh:129-134 — `_comp_dumpfile` defaults to
766 // `${ZDOTDIR:-$HOME}/.zcompdump` and is overridden by `-d FILE`.
767 // `typeset -g NAME=VALUE` assigns unconditionally, so an explicit
768 // `-d` always wins; without one the default only fills an
769 // empty/absent value.
770 match dumpfile {
771 Some(f) if !f.is_empty() => {
772 let _ = crate::ported::params::setsparam("_comp_dumpfile", f); // sh:131
773 }
774 _ => {
775 if crate::ported::params::getsparam("_comp_dumpfile")
776 .map(|s| s.is_empty())
777 .unwrap_or(true)
778 {
779 let _ = crate::ported::params::setsparam(
780 "_comp_dumpfile",
781 &default_dumpfile_path().to_string_lossy(),
782 ); // sh:133
783 }
784 }
785 }
786
787 // sh:138-172 — `typeset -gHa _comp_options` + the option list.
788 declare_global("_comp_options", PM_ARRAY, PM_HIDEVAL);
789 crate::ported::params::setaparam(
790 "_comp_options",
791 COMP_OPTIONS.iter().map(|s| s.to_string()).collect(),
792 );
793
794 // sh:180-190 — `typeset -gH _comp_setup='…'`.
795 declare_global("_comp_setup", 0, PM_HIDEVAL);
796 let _ = crate::ported::params::setsparam("_comp_setup", COMP_SETUP_EVAL);
797
798 // sh:195-197 — `typeset -ga compprefuncs comppostfuncs` then both
799 // reset to empty.
800 init_comp_funcs_arrays();
801
802 // compdump sh:134-135 — `typeset -gUa _comp_assocs`. compdump
803 // writes those two lines into every dump file, and `compinit -C`
804 // reaches them by sourcing it (sh:493 `builtin . "$_comp_dumpfile"`).
805 // zshrs parses the dump into its own cache instead of sourcing it,
806 // so the declaration has to be made here to reach the same state.
807 declare_global("_comp_assocs", PM_ARRAY, PM_UNIQUE);
808}
809
810/// sh:337 — `[[ -n "$autol" ]] && autoload -rUz "$func"`.
811///
812/// compinit registers every scanned completion file with `compdef -na
813/// "${_i_name}" …` (sh:541), and the `-a` in that call is what makes
814/// `compdef` run `autoload -rUz "$func"` at sh:337. The dump-file fast
815/// path (sh:493 `builtin . "$_comp_dumpfile"`) reaches the same state
816/// from compdump's single `autoload -Uz …` line. Either way a real zsh
817/// finishes `compinit` with an autoload stub in `shfunctab` for EVERY
818/// completer basename found in `$fpath`, and completers read that table:
819/// `_tmux` builds its sub-command list from
820/// `${(M)${(k)functions}:#_tmux-*}` (_tmux sh:1967).
821///
822/// zshrs bulk-loads `$_comps` from its own cache and materializes bodies
823/// lazily, so it skipped this step entirely; `${(k)functions}` held only
824/// the functions the session had actually defined.
825///
826/// Only names with no existing `shfunctab` entry get a stub, mirroring
827/// `bin_functions`' behaviour of leaving an already-defined function
828/// alone. Flags match `autoload -rUz`: `PM_UNDEFINED | PM_UNALIASED`
829/// (c:Src/builtin.c:3352-3355) and `PM_ZSHSTORED` (c:3372). Returns the
830/// number of stubs added.
831pub fn register_autoload_stubs<I, S>(names: I) -> usize
832where
833 I: IntoIterator<Item = S>,
834 S: AsRef<str>,
835{
836 use crate::ported::zsh_h::{PM_UNALIASED, PM_UNDEFINED, PM_ZSHSTORED};
837 let flags = (PM_UNDEFINED | PM_UNALIASED | PM_ZSHSTORED) as i32;
838 let mut added = 0usize;
839 let names = names.into_iter();
840 let Ok(mut tab) = crate::ported::hashtable::shfunctab_lock().write() else {
841 return 0;
842 };
843 tab.reserve(names.size_hint().0);
844 for name in names {
845 let name = name.as_ref();
846 if name.is_empty() || tab.contains_key(name) {
847 continue;
848 }
849 let mut stub = crate::ported::hashtable::shfunc_autoload(name);
850 stub.node.flags = flags;
851 tab.add(stub);
852 added += 1;
853 }
854 added
855}
856
857/// Autoload-stub names contributed by a completed scan — every file
858/// whose header was `#compdef` or `#autoload`, which is exactly the set
859/// compinit hands to `compdef -na` / `autoload -rUz` at sh:537-547.
860pub fn autoload_stub_names(result: &CompInitResult) -> Vec<&str> {
861 result
862 .files
863 .iter()
864 .filter(|f| matches!(f.def, CompFileDef::CompDef(_) | CompFileDef::Autoload(_)))
865 .map(|f| f.name.as_str())
866 .collect()
867}
868
869/// sh:516 `builtin . "$_comp_dumpfile"` — the autoload half of sourcing
870/// a dump file, extracted without executing it.
871///
872/// When a dump file exists, upstream compinit does NOT scan `$fpath` at
873/// all (sh:491-518 sources the dump and sets `_i_done`, which skips the
874/// whole sh:523-550 scan). The names in `${(k)functions}` after such a
875/// compinit therefore come from the dump's `autoload` lines, and
876/// compdump writes those from a DIFFERENT rule than compinit's scan:
877///
878/// compdump:113 `_d_als=($^fpath/(${(o~j.|.)$(typeset +fm '_*')})(N:t))`
879///
880/// i.e. every currently-DEFINED function whose name starts with `_` and
881/// which also has a file somewhere in `$fpath` — no `#compdef` /
882/// `#autoload` first line required. That is a strict superset of the
883/// header-driven set `autoload_stub_names` derives from a scan: a
884/// headerless helper the user's rc autoloaded by hand is in the dump but
885/// invisible to any header-based scan. It can even name a function whose
886/// file has since been deleted from `$fpath`, because the dump is a
887/// snapshot (`autoload` on a missing file still creates the stub and only
888/// errors when called).
889///
890/// Two line shapes are produced by compdump and both are parsed here:
891/// compdump:118-129 one `autoload -Uz a b c \` + continuations, and
892/// compdump:135-138 one `autoload -Uz <opts> <name>` per `$_compautos`.
893/// Word-splitting mirrors the shell's: `autoload` must be the first word,
894/// tokens starting with `-` or `+` are option words, the rest are names.
895pub fn dump_autoload_names(path: &Path) -> Vec<String> {
896 let Ok(text) = std::fs::read_to_string(path) else {
897 return Vec::new();
898 };
899 let mut names = Vec::new();
900 let mut in_autoload = false;
901 for line in text.lines() {
902 let mut rest = line.trim();
903 if !in_autoload {
904 let Some(tail) = rest.strip_prefix("autoload") else {
905 continue;
906 };
907 // "autoloadfoo" is not the `autoload` command.
908 if !tail.is_empty() && !tail.starts_with(|c: char| c.is_ascii_whitespace()) {
909 continue;
910 }
911 rest = tail.trim_start();
912 }
913 // A trailing `\` continues the command onto the next line.
914 in_autoload = rest.ends_with('\\');
915 if in_autoload {
916 rest = rest[..rest.len() - 1].trim_end();
917 }
918 for word in rest.split_ascii_whitespace() {
919 if word.starts_with('-') || word.starts_with('+') {
920 continue; // `-Uz`, `+X`, …
921 }
922 names.push(word.to_string());
923 }
924 }
925 names
926}
927
928/// The five association tables a dump file defines (compdump sh:31-72).
929///
930/// `IndexMap`, not `HashMap`: compdump writes every table in `${(ok)…}`
931/// order and sourcing it reproduces exactly that insertion order, which
932/// `${(k)_comps}` and — for `_patcomps`/`_postpatcomps` — pattern-match
933/// precedence are both observable through.
934#[derive(Debug, Default)]
935pub struct DumpTables {
936 /// `_comps=(…)` — command -> completer
937 pub comps: indexmap::IndexMap<String, String>,
938 /// `_services=(…)` — command -> service name
939 pub services: indexmap::IndexMap<String, String>,
940 /// `_patcomps=(…)` — pattern -> completer (tried before `_comps`)
941 pub patcomps: indexmap::IndexMap<String, String>,
942 /// `_postpatcomps=(…)` — pattern -> completer (tried after `_comps`)
943 pub postpatcomps: indexmap::IndexMap<String, String>,
944 /// `_compautos=(…)` — autoload name -> extra `autoload` options
945 pub compautos: indexmap::IndexMap<String, String>,
946}
947
948/// Split one dump line into words the way the shell would, honouring the
949/// single-quoting `${(qq)}` applies in compdump (sh:38, 45, 52, 59, 64, 70).
950///
951/// `(qq)` renders an embedded `'` as `'\''`, so `'brew` is written
952/// `''\''brew'` — three concatenated segments. Adjacent segments join into
953/// one word, which is what makes plain `split_whitespace` wrong here.
954fn split_quoted_words(line: &str) -> Vec<String> {
955 let mut out = Vec::new();
956 let mut cur = String::new();
957 let mut started = false;
958 let mut it = line.chars();
959 while let Some(c) = it.next() {
960 match c {
961 ' ' | '\t' => {
962 if started {
963 out.push(std::mem::take(&mut cur));
964 started = false;
965 }
966 }
967 '\'' => {
968 started = true;
969 for d in it.by_ref() {
970 if d == '\'' {
971 break;
972 }
973 cur.push(d);
974 }
975 }
976 '\\' => {
977 started = true;
978 if let Some(d) = it.next() {
979 cur.push(d);
980 }
981 }
982 _ => {
983 started = true;
984 cur.push(c);
985 }
986 }
987 }
988 if started {
989 out.push(cur);
990 }
991 out
992}
993
994/// sh:494 `builtin . "$_comp_dumpfile"` — the association-table half of
995/// sourcing a dump file, extracted without executing it.
996///
997/// On the `-C` path (sh:493-496) upstream sources the dump and sets
998/// `_i_done`, and sh:501's `if [[ -z "$_i_done" ]]` then skips the whole
999/// sh:504-528 `$fpath` scan. The dump is therefore the SOLE definition of
1000/// `_comps`/`_services`/`_patcomps`/`_postpatcomps`/`_compautos` for that
1001/// session — nothing else contributes a single key.
1002///
1003/// zshrs substituted its own SQLite cache for that payload, which is
1004/// refreshed on a different schedule than the shared `.zcompdump` and can
1005/// hold a partial scan. On this host the cache carried 1849 `_comps`
1006/// entries against the dump's 51745, so `$_comps[zpwr]` (and `cargo`,
1007/// `brew`, …) was simply absent, `_dispatch` fell through to `-default-`
1008/// and `zpwr <TAB>` completed FILES where zsh runs `_zpwr`.
1009///
1010/// compdump writes each table as a bare `NAME=(` line, one `'key' 'value'`
1011/// pair per line, and a bare `)` (sh:31-72); this parses exactly that
1012/// shape and ignores everything else in the file.
1013pub fn dump_assoc_tables(path: &Path) -> Option<DumpTables> {
1014 let text = std::fs::read_to_string(path).ok()?;
1015 let mut tables = DumpTables::default();
1016 let mut open: Option<usize> = None;
1017 for line in text.lines() {
1018 if let Some(idx) = open {
1019 if line.trim_end() == ")" {
1020 open = None;
1021 continue;
1022 }
1023 let words = split_quoted_words(line);
1024 if words.len() >= 2 {
1025 let table = match idx {
1026 0 => &mut tables.comps,
1027 1 => &mut tables.services,
1028 2 => &mut tables.patcomps,
1029 3 => &mut tables.postpatcomps,
1030 _ => &mut tables.compautos,
1031 };
1032 table.insert(words[0].clone(), words[1].clone());
1033 }
1034 continue;
1035 }
1036 open = match line.trim_end() {
1037 "_comps=(" => Some(0),
1038 "_services=(" => Some(1),
1039 "_patcomps=(" => Some(2),
1040 "_postpatcomps=(" => Some(3),
1041 "_compautos=(" => Some(4),
1042 _ => None,
1043 };
1044 }
1045 Some(tables)
1046}
1047
1048/// Default `$_comp_dumpfile` path (sh:129-134). User can override
1049/// via `compinit -d <file>`; without that, use `${ZDOTDIR:-$HOME}`
1050/// + `/.zcompdump`.
1051pub fn default_dumpfile_path() -> PathBuf {
1052 let home = std::env::var("ZDOTDIR")
1053 .ok()
1054 .filter(|s| !s.is_empty())
1055 .or_else(|| std::env::var("HOME").ok())
1056 .unwrap_or_else(|| ".".to_string());
1057 PathBuf::from(home).join(".zcompdump")
1058}
1059
1060/// Publish the standard ZLE rebind set to the dispatcher hooks
1061/// (sh:555-560). For each `complete-word` family widget + a
1062/// conditional `menu-select` (when `zsh/complist` is loaded), bind
1063/// it to `_main_complete` via `zle -C`. Returns the count of
1064/// successful binds.
1065pub fn install_standard_complete_widgets() -> usize {
1066 // `zle` is a builtin, not a shell function, so it must be invoked
1067 // through the builtin entry (`bin_zle_complete`) — NOT
1068 // `dispatch_function_call`, which only resolves shell functions and
1069 // silently returns None for a builtin name, leaving every widget
1070 // unbound (the whole compsys engine then never fires on Tab).
1071 let empty_ops = crate::ported::zsh_h::options {
1072 ind: [0u8; crate::ported::zsh_h::MAX_OPS],
1073 args: Vec::new(),
1074 argscount: 0,
1075 argsalloc: 0,
1076 };
1077 let mut count = 0usize;
1078 tracing::debug!(target: "compsys_args", "install_standard_complete_widgets ENTER");
1079 for w in STANDARD_COMPLETE_WIDGETS {
1080 // `zle -C <w> .<w> _main_complete` — args are post-flag:
1081 // [target-thingy, base-comp-widget, completion-func].
1082 let args = [
1083 w.to_string(),
1084 format!(".{}", w),
1085 "_main_complete".to_string(),
1086 ];
1087 let rc_w = crate::ported::zle::zle_thingy::bin_zle_complete("zle", &args, &empty_ops, 0);
1088 tracing::debug!(target: "compsys_args", widget = %w, rc_w, "zle -C standard widget");
1089 if rc_w == 0 {
1090 count += 1;
1091 }
1092 }
1093 // sh:560 — `zle -C menu-select .menu-select _main_complete` (only
1094 // succeeds when the `.menu-select` base widget exists, i.e.
1095 // `zsh/complist` is loaded; bin_zle_complete returns 1 otherwise).
1096 {
1097 let args = [
1098 "menu-select".to_string(),
1099 ".menu-select".to_string(),
1100 "_main_complete".to_string(),
1101 ];
1102 let rc_w = crate::ported::zle::zle_thingy::bin_zle_complete("zle", &args, &empty_ops, 0);
1103 tracing::debug!(target: "compsys_args", widget = "menu-select", rc_w, "zle -C standard widget");
1104 if rc_w == 0 {
1105 count += 1;
1106 }
1107 }
1108 count
1109}
1110
1111/// `zmodload -i NAME` — mark a statically-linked module booted.
1112///
1113/// zsh has no `zmodload` call for these: referencing an autoloadable
1114/// parameter or builtin loads its module implicitly (`autoparamfn` /
1115/// `autobinfn`). zshrs registers those parameters and builtins at init
1116/// without going through the module, so the module's `MOD_INIT_B` bit —
1117/// the one `zmodload` / `zmodload -L` list on — never got set. Routing
1118/// the implicit load through the real builtin reaches the same state.
1119fn load_module_i(name: &str) {
1120 let mut ops = crate::ported::zsh_h::options {
1121 ind: [0u8; crate::ported::zsh_h::MAX_OPS],
1122 args: Vec::new(),
1123 argscount: 0,
1124 argsalloc: 0,
1125 };
1126 ops.ind[b'i' as usize] = 1;
1127 let _ = crate::ported::module::bin_zmodload("zmodload", &[name.to_string()], &ops, 0);
1128}
1129
1130/// sh:201 — `: $funcstack`, with the upstream comment "Loading it now
1131/// ensures that the `funcstack' parameter is always correct."
1132///
1133/// `funcstack` is one of `zsh/parameter`'s autoloadable parameters
1134/// (Src/Modules/parameter.mdd), so that bare `:` command is a module
1135/// load in disguise — after any compinit, a real zsh lists
1136/// `zsh/parameter` in `zmodload -L`.
1137///
1138/// It is a SINGLE-FEATURE load, not a `zmodload zsh/parameter`: reading
1139/// the name runs `loadparamnode` → `ensurefeature(mn, "p:", "funcstack")`
1140/// (c:Src/params.c:568 → c:Src/module.c:3426-3432), which enables only
1141/// `p:funcstack`. The module's other parameters (`commands`, `aliases`,
1142/// …) stay PM_AUTOLOAD stubs — verified against zsh 5.9:
1143/// `: ${#aliases}; f(){ local -A +h commands; print ${(t)commands} }; f`
1144/// prints `association-local`, while the same after an explicit
1145/// `zmodload zsh/parameter` prints `association-local-special`.
1146/// Routing this through the full `zmodload` marked every sibling
1147/// materialized, so it must go through `ensurefeature`.
1148pub fn touch_funcstack_param() {
1149 // c:Src/params.c:565-568 — `loadparamnode` on a PM_AUTOLOAD stub:
1150 // `(void)ensurefeature(mn, "p:", nam)`. `mark_module_param_used` is
1151 // zshrs's hook for exactly that (vm_helper.rs), and it boots the
1152 // module, so `zmodload -L` still lists `zsh/parameter` afterwards.
1153 crate::vm_helper::mark_module_param_used("funcstack");
1154}
1155
1156/// sh:564-569 — when the configured `completer` chain includes
1157/// `_expand` AND `^i` is currently bound to `expand-or-complete`,
1158/// rebind `^i` to `complete-word` so users don't get unexpected
1159/// glob expansion on TAB.
1160pub fn maybe_rebind_tab_for_expand() {
1161 // sh:572 — `zstyle -a ':completion:' completer _i_line`. `zstyle` is
1162 // `zsh/zutil`'s builtin (Src/Modules/zutil.mdd), so this line is what
1163 // pulls the module in on every compinit and leaves it in
1164 // `zmodload -L`. zshrs answers the query through the native
1165 // `lookupstyle` below instead of the builtin, so the module was never
1166 // marked booted: `zmodload -L` printed four lines against zsh's six.
1167 load_module_i("zsh/zutil");
1168 // sh:565 is the literal context `':completion:'` — NOT
1169 // `:completion:$curcontext:`. The two agree under a `:completion:*`
1170 // fixture, so the difference only shows with a scoped zstyle, but the
1171 // spec string is the unadorned one.
1172 let completers = crate::ported::modules::zutil::lookupstyle(":completion:", "completer");
1173 // sh:566 `(( ${_i_line[(i)_expand]} <= ${#_i_line} ))` is an EXACT
1174 // element match, so a named completer such as `_expand:foo` does not
1175 // arm the rebind in zsh either.
1176 let has_expand = completers.iter().any(|c| c == "_expand");
1177 if !has_expand {
1178 return;
1179 }
1180 // sh:563-564 — `bindkey '^i' | IFS=$' \t' read -A _i_line` /
1181 // `[[ ${_i_line[2]} = expand-or-complete ]]`. `_i_line[2]` is the
1182 // widget name `bindkey` prints after the quoted key sequence, so the
1183 // guard asks exactly what `bin_bindkey_list` resolves at
1184 // `zle_keymap.rs:1840-1846`: `keybind(km, getkeystring("^i"))` on the
1185 // keymap `bindkey` picks with no `-M`/-e/-v/-a, which
1186 // `bin_bindkey` (zle_keymap.rs:1250-1263) fixes at `main`. Reading the
1187 // table directly instead of running the builtin keeps `bindkey`'s
1188 // listing off stdout. Without this guard a user who had already put
1189 // their own widget on TAB got it silently clobbered by every compinit.
1190 let seq = crate::ported::zle::zle_bindings::getkeystring("^i");
1191 // `openkeymap` misses until `default_bindings()` has run; mirror the
1192 // same emptiness gate `bin_bindkey` uses (zle_keymap.rs:1121-1127) so
1193 // a compinit that precedes any `bindkey` call still sees the defaults,
1194 // and an already-built keymap is never rebuilt (that would wipe the
1195 // user's bindings).
1196 let km = crate::ported::zle::zle_keymap::openkeymap("main").or_else(|| {
1197 crate::ported::zle::zle_keymap::default_bindings();
1198 crate::ported::zle::zle_keymap::openkeymap("main")
1199 });
1200 let bound = km
1201 .and_then(|km| crate::ported::zle::zle_keymap::keybind(&km, &seq).0)
1202 .map(|t| t.nam);
1203 if bound.as_deref() != Some("expand-or-complete") {
1204 return;
1205 }
1206 // sh:568 — `bindkey '^i' complete-word`. `bindkey` is a BUILTIN, so it
1207 // must go through `bin_bindkey`; `dispatch_function_call` resolves only
1208 // shell functions and returned None here, making the whole rebind a
1209 // silent no-op (see the same warning in
1210 // `install_standard_complete_widgets`).
1211 let empty_ops = crate::ported::zsh_h::options {
1212 ind: [0u8; crate::ported::zsh_h::MAX_OPS],
1213 args: Vec::new(),
1214 argscount: 0,
1215 argsalloc: 0,
1216 };
1217 let bk_args = ["^i".to_string(), "complete-word".to_string()];
1218 let _ = crate::ported::zle::zle_keymap::bin_bindkey("bindkey", &bk_args, &empty_ops, 0);
1219}
1220
1221// `compaudit` lives in its own file (`src/compsys/ported/compaudit.rs`)
1222// per zsh upstream's layout (`Completion/compaudit` is a sibling of
1223// `Completion/compinit`). Re-export the entry point so existing
1224// compinit-facing callers don't need to change.
1225pub use super::compaudit::{compaudit, CompauditError};
1226
1227/// Completion definition from #compdef line
1228#[derive(Clone, Debug)]
1229pub enum CompDef {
1230 /// Regular command completion: #compdef cmd1 cmd2 ...
1231 Commands(Vec<String>),
1232 /// Pattern completion: #compdef -p 'pattern' [pattern...]
1233 Pattern(Vec<String>),
1234 /// Post-pattern completion: #compdef -P 'pattern' [pattern...]
1235 PostPattern(Vec<String>),
1236 /// One header that registers MORE THAN ONE kind, because `-N`/`-p`/`-P`
1237 /// switch the target table for the words that FOLLOW them (compinit
1238 /// sh:384-420). `_gcc`'s header is the canonical case:
1239 /// `#compdef gcc g++ … -value-,CFLAGS,-default- … -P gcc-* -P g++-* -P c++-*`
1240 /// — eight command names, six `-value-` contexts, then three
1241 /// post-patterns, all from one line.
1242 Mixed {
1243 commands: Vec<String>,
1244 patterns: Vec<String>,
1245 postpatterns: Vec<String>,
1246 },
1247 /// Key binding: #compdef -k style key1 key2 ...
1248 KeyBinding { style: String, keys: Vec<String> },
1249 /// Widget key binding: #compdef -K widget style key
1250 WidgetKey {
1251 widget: String,
1252 style: String,
1253 key: String,
1254 },
1255}
1256
1257/// Parsed completion file
1258#[derive(Clone, Debug)]
1259pub struct CompFile {
1260 /// Full path to the file
1261 pub path: PathBuf,
1262 /// Function name (filename without path)
1263 pub name: String,
1264 /// What this file defines
1265 pub def: CompFileDef,
1266 /// Full file body (read during scan for caching)
1267 pub body: Option<String>,
1268}
1269
1270/// What a completion file defines
1271#[derive(Clone, Debug)]
1272pub enum CompFileDef {
1273 /// #compdef - completion function
1274 CompDef(CompDef),
1275 /// #autoload - helper function with options
1276 Autoload(Vec<String>),
1277 None,
1278}
1279
1280/// Result of compinit scan
1281#[derive(Debug, Default)]
1282pub struct CompInitResult {
1283 /// Command -> function mapping (_comps)
1284 pub comps: HashMap<String, String>,
1285 /// Command -> service mapping (_services)
1286 pub services: HashMap<String, String>,
1287 /// Pattern -> function mapping (_patcomps)
1288 pub patcomps: HashMap<String, String>,
1289 /// Post-pattern -> function mapping (_postpatcomps)
1290 pub postpatcomps: HashMap<String, String>,
1291 /// Autoload functions with options (_compautos)
1292 pub compautos: HashMap<String, String>,
1293 /// All scanned files
1294 pub files: Vec<CompFile>,
1295 /// `#compdef -k <style> <key>...` widget bindings collected from file
1296 /// headers: (func, style, keys). Applied by the foreground compinit
1297 /// caller via `zle -C` + `bindkey` (upstream compinit sh:356-379) —
1298 /// the scan itself may run on a background thread and must not touch
1299 /// keymaps.
1300 pub keybindings: Vec<(String, String, Vec<String>)>,
1301 /// `#compdef -K <widget> <style> <key>` triplets: (widget, style, key, func).
1302 pub widgetkeys: Vec<(String, String, String, String)>,
1303 /// Scan duration
1304 pub scan_time_ms: u64,
1305 /// Number of directories scanned
1306 pub dirs_scanned: usize,
1307 /// Number of files scanned
1308 pub files_scanned: usize,
1309}
1310
1311/// Parse the first line of a completion file
1312///
1313/// Handles all #compdef variants:
1314/// - `#compdef cmd1 cmd2` - regular commands
1315/// - `#compdef - cmd1 cmd2` - bare hyphen + commands (hyphen maps to '-')
1316/// - `#compdef -default-` - special context entries
1317/// - `#compdef -value-,VAR,-default-` - value context entries
1318/// - `#compdef -p pattern` - pattern completions
1319/// - `#compdef -P pattern` - post-pattern completions
1320/// - `#compdef -k style key` - key bindings
1321/// - `#compdef -K widget style key` - widget key bindings
1322fn parse_first_line(line: &str) -> CompFileDef {
1323 let line = line.trim();
1324
1325 if let Some(rest) = line.strip_prefix("#compdef") {
1326 let rest = rest.trim();
1327 if rest.is_empty() {
1328 return CompFileDef::None;
1329 }
1330
1331 let parts: Vec<&str> = rest.split_whitespace().collect();
1332 if parts.is_empty() {
1333 return CompFileDef::None;
1334 }
1335
1336 // sh:534-539 — ONLY a leading `-[pPkK]` (optionally `n`-suffixed)
1337 // is passed to compdef as an option:
1338 // if [[ $_i_line[1] = -[pPkK](n|) ]]; then
1339 // compdef ${_i_line[1]}na "$name" "${(@)_i_line[2,-1]}"
1340 // else
1341 // compdef -na "$name" "${_i_line[@]}"
1342 // Everything else — `-n`, `-m`, `-default-`, a bare `-` — is an
1343 // ordinary positional word, not a flag. (`_squishy`'s
1344 // `#compdef squishy "python -m squishy"` is read by `read -rA`,
1345 // which does no quote processing, so zsh really does register a
1346 // command literally named `-m`.)
1347 let leading = parts[0].strip_suffix('n').filter(|f| f.len() == 2);
1348 match leading.unwrap_or(parts[0]) {
1349 "-k" if parts.len() >= 3 => CompFileDef::CompDef(CompDef::KeyBinding {
1350 style: parts[1].to_string(),
1351 keys: parts[2..].iter().map(|s| s.to_string()).collect(),
1352 }),
1353 "-K" if parts.len() >= 4 => CompFileDef::CompDef(CompDef::WidgetKey {
1354 widget: parts[1].to_string(),
1355 style: parts[2].to_string(),
1356 key: parts[3].to_string(),
1357 }),
1358 flag => {
1359 // sh:384-420 — the positional loop. `type` starts at whatever
1360 // the leading flag set (sh:277-285) and is RE-SET by any `-N`
1361 // / `-p` / `-P` met along the way, so one header can feed
1362 // `_comps`, `_patcomps` and `_postpatcomps` at once. Handling
1363 // the flag only in first position lost every trailing `-P` in
1364 // the tree — `_gcc`'s `gcc-*`/`g++-*`/`c++-*`, `_lua`'s
1365 // `lua[0-9.-]##`, `_ruby`, `_php`, `_shasum`, `_rmlint`,
1366 // `_urls`, `_directories`, `_locales`, `_ccache` … 15 of
1367 // zsh's 25 `_postpatcomps` entries were missing, and the
1368 // patterns were wrongly registered as literal command names
1369 // in `_comps` instead.
1370 let mut ty = match flag {
1371 "-p" => 1,
1372 "-P" => 2,
1373 _ => 0,
1374 };
1375 let start = usize::from(ty != 0);
1376 let mut commands: Vec<String> = Vec::new();
1377 let mut patterns: Vec<String> = Vec::new();
1378 let mut postpatterns: Vec<String> = Vec::new();
1379 for word in &parts[start..] {
1380 match *word {
1381 "-N" => ty = 0, // sh:385-386
1382 "-p" => ty = 1, // sh:387-388
1383 "-P" => ty = 2, // sh:389-390
1384 _ => match ty {
1385 1 => patterns.push(word.to_string()),
1386 2 => postpatterns.push(word.to_string()),
1387 _ => commands.push(word.to_string()),
1388 },
1389 }
1390 }
1391 match (
1392 commands.is_empty(),
1393 patterns.is_empty(),
1394 postpatterns.is_empty(),
1395 ) {
1396 (true, true, true) => CompFileDef::None,
1397 (false, true, true) => CompFileDef::CompDef(CompDef::Commands(commands)),
1398 (true, false, true) => CompFileDef::CompDef(CompDef::Pattern(patterns)),
1399 (true, true, false) => CompFileDef::CompDef(CompDef::PostPattern(postpatterns)),
1400 _ => CompFileDef::CompDef(CompDef::Mixed {
1401 commands,
1402 patterns,
1403 postpatterns,
1404 }),
1405 }
1406 }
1407 }
1408 } else if let Some(rest) = line.strip_prefix("#autoload") {
1409 let opts: Vec<String> = rest.split_whitespace().map(|s| s.to_string()).collect();
1410 CompFileDef::Autoload(opts)
1411 } else {
1412 CompFileDef::None
1413 }
1414}
1415
1416/// Check if a string is a zsh completion context entry
1417/// Context entries are like: -default-, -redirect-, -command-, -value-,VAR,-default-
1418/// Also handles service syntax: -redirect-,<,bunzip2=bunzip2
1419fn is_context_entry(s: &str) -> bool {
1420 if !s.starts_with('-') {
1421 return false;
1422 }
1423 // Strip service suffix for checking
1424 let base = s.split('=').next().unwrap_or(s);
1425
1426 // Check if it's a known context pattern:
1427 // 1. Ends with '-' like -default-, -redirect-
1428 // 2. Contains comma (context specifiers like -redirect-,<,bunzip2 or -value-,VAR,-default-)
1429 // 3. But NOT single letter options like -p, -P, -k, -K, -n
1430 if base.len() <= 2 {
1431 return base == "-"; // bare hyphen is a context entry
1432 }
1433
1434 base.ends_with('-') || base.contains(',')
1435}
1436
1437/// Scan a single completion file - reads full body for caching
1438fn scan_file(path: &Path) -> Option<CompFile> {
1439 let name = path.file_name()?.to_string_lossy().to_string();
1440
1441 // Must start with underscore
1442 if !name.starts_with('_') {
1443 return None;
1444 }
1445
1446 // Skip certain patterns
1447 if name.contains(';')
1448 || name.contains('|')
1449 || name.contains('&')
1450 || name.ends_with('~')
1451 || name.ends_with(".zwc")
1452 {
1453 return None;
1454 }
1455
1456 // Read entire file at once (will be cached in SQLite)
1457 let body = fs::read_to_string(path).ok()?;
1458
1459 // Parse first line for directive
1460 let first_line = body.lines().next().unwrap_or("");
1461 let def = parse_first_line(first_line);
1462
1463 Some(CompFile {
1464 path: path.to_path_buf(),
1465 name,
1466 def,
1467 body: Some(body),
1468 })
1469}
1470
1471/// Scan a directory for completion files (parallel)
1472fn scan_directory(dir: &Path) -> Vec<CompFile> {
1473 let entries = match fs::read_dir(dir) {
1474 Ok(e) => e,
1475 Err(_) => return Vec::new(),
1476 };
1477
1478 let mut paths: Vec<PathBuf> = entries
1479 .filter_map(|e| e.ok())
1480 .map(|e| e.path())
1481 .filter(|p| p.is_file())
1482 .collect();
1483 // sh:507 — `for _i_file in $_i_dir/^(…)(N)`. A zsh glob yields its
1484 // matches SORTED, and that order decides which file claims a command
1485 // name first (compdef -n keeps the first claim, sh:393). `read_dir`
1486 // returns filesystem order, so without this sort the winner inside a
1487 // directory varied by inode layout.
1488 //
1489 // The comparator has to be zsh's, not Rust's byte-wise `Ord`:
1490 // c:Src/glob.c:1976 sorts matches with `gmatchcmp`, whose GS_NAME arm
1491 // (c:946) is `zstrcmp(…, 0)` → `strcoll` under the current LC_COLLATE.
1492 // Byte order put `_act-runner` (`-` = 0x2D) ahead of `_act_runner`
1493 // (`_` = 0x5F) while en_US.UTF-8 collation puts `_act_runner` first,
1494 // so ~40 commands whose completer has a `-`/`_` twin in the same
1495 // directory (`act_runner`, `amdgpu_top`, `cloud_sql_proxy`, `dh_make`,
1496 // …) got the WRONG file's function registered in `$_comps`.
1497 paths.sort_by(|a, b| {
1498 let name = |p: &Path| {
1499 p.file_name()
1500 .map(|f| f.to_string_lossy().into_owned())
1501 .unwrap_or_default()
1502 };
1503 crate::ported::sort::zstrcmp(&name(a), &name(b), 0)
1504 });
1505
1506 // Parallel scan of files within directory
1507 paths.par_iter().filter_map(|p| scan_file(p)).collect()
1508}
1509
1510/// Initialize the completion system by scanning fpath
1511///
1512/// This is the main entry point - replaces the zsh compinit function.
1513/// Uses rayon for parallel directory and file scanning.
1514/// Apply the `#compdef -k`/`-K` widget key bindings a scan collected —
1515/// the in-shell half the scan can't do itself (it may run on a background
1516/// thread). Mirrors compdef's key branch: sh:365 `zle -C <func> .<style>
1517/// <func>`, sh:378 `bindkey <key> <func>`. Without this, header-declared
1518/// completion widgets parsed but never bound — `^X?` (_complete_debug,
1519/// `#compdef -k complete-word \C-x?`) and `^Xh` (_complete_help)
1520/// self-inserted literally instead of running.
1521pub fn apply_keybindings(result: &CompInitResult) {
1522 for (func, style, keys) in &result.keybindings {
1523 for key in keys {
1524 install_comp_keybinding(func, style, key, func);
1525 }
1526 }
1527 for (widget, style, key, func) in &result.widgetkeys {
1528 install_comp_keybinding(widget, style, key, func);
1529 }
1530}
1531
1532/// One `#compdef -k`/`-K` binding: sh:346/365 `zle -C <widget> .<style>
1533/// <func>` + sh:351/378 `bindkey <key> <widget>`. Goes through the builtin
1534/// entries directly — `dispatch_function_call` resolves only shell
1535/// functions and silently no-ops for builtins (see
1536/// install_standard_complete_widgets).
1537fn install_comp_keybinding(widget: &str, style: &str, key: &str, func: &str) {
1538 // `zle -C` needs the thingy table populated: bin_zle_complete looks the
1539 // base comp-widget up by its dotted name (`.complete-word`,
1540 // c:Src/Zle/zle_thingy.c:609-614 `rthingy`) and returns 1 when it is
1541 // absent. In C that table is filled by `init_thingies()` at zsh/zle
1542 // module boot (c:zle_thingy.c:1022, reached from zle_main.c:2252), so
1543 // every `zle -C` compdef runs sees it. zshrs fills it LAZILY — from
1544 // `bin_zle` (zle_thingy.rs:673) and from a `$widgets` read
1545 // (zleparameter.rs:63) — and this call site bypasses both by invoking
1546 // `bin_zle_complete` directly, so on a fresh shell the table was still
1547 // empty and EVERY `#compdef -k`/`-K` widget silently failed to bind:
1548 // `_complete_debug`, `_complete_help`, `_complete_tag`,
1549 // `_correct_word`, `_correct_filename`, `_expand_word`,
1550 // `_expand_alias`, `_list_expansions`, `_next_tags`, `_read_comp`,
1551 // `_most_recent_file`, `_history-complete-{older,newer}`,
1552 // `_bash_{complete-word,list-choices}` — 15 widgets zsh has and zshrs
1553 // did not (401 vs 386 `${(k)widgets}` after the same `compinit -C -d`).
1554 // The `bindkey` half below DID run, so `^X?` was bound to a widget that
1555 // did not exist. Trigger the same lazy init `bin_zle` would; it is
1556 // idempotent (per-name `contains_key` guard inside init_thingies).
1557 crate::ported::zle::zle_thingy::init_thingies();
1558 let empty_ops = crate::ported::zsh_h::options {
1559 ind: [0u8; crate::ported::zsh_h::MAX_OPS],
1560 args: Vec::new(),
1561 argscount: 0,
1562 argsalloc: 0,
1563 };
1564 let style_dotted = if style.starts_with('.') {
1565 style.to_string()
1566 } else {
1567 format!(".{}", style)
1568 };
1569 // sh:346/365 — `zle -C <widget> <.style> <func>`.
1570 let zle_args = [widget.to_string(), style_dotted, func.to_string()];
1571 let _ = crate::ported::zle::zle_thingy::bin_zle_complete("zle", &zle_args, &empty_ops, 0);
1572 // sh:351/378 — `bindkey <key> <widget>`.
1573 let bk_args = [key.to_string(), widget.to_string()];
1574 let _ = crate::ported::zle::zle_keymap::bin_bindkey("bindkey", &bk_args, &empty_ops, 0);
1575}
1576
1577/// The `#compdef -k`/`-K` headers shipped in the upstream Completion tree —
1578/// the fixed set compinit produces for a stock install (each cited from its
1579/// file's first line). Installed synchronously with the standard widget
1580/// rebind because the background scan's results merge lazily (and the
1581/// cached path skips the scan entirely); user fpath files with -k headers
1582/// are additionally collected by the scan into CompInitResult.keybindings.
1583const STANDARD_COMP_KEYBINDINGS: &[(&str, &str, &str, &str)] = &[
1584 // (widget, style, key, func)
1585 (
1586 "_complete_debug",
1587 "complete-word",
1588 "\u{18}?",
1589 "_complete_debug",
1590 ), // Base/Widget/_complete_debug:1 \C-x?
1591 (
1592 "_complete_help",
1593 "complete-word",
1594 "\u{18}h",
1595 "_complete_help",
1596 ), // Base/Widget/_complete_help:1 \C-xh
1597 ("_complete_tag", "complete-word", "\u{18}t", "_complete_tag"), // Base/Widget/_complete_tag:1 \C-xt
1598 (
1599 "_correct_filename",
1600 "complete-word",
1601 "\u{18}C",
1602 "_correct_filename",
1603 ), // _correct_filename:1 \C-xC
1604 ("_correct_word", "complete-word", "\u{18}c", "_correct_word"), // _correct_word:1 \C-xc
1605 ("_read_comp", "complete-word", "\u{18}\u{12}", "_read_comp"), // _read_comp:1 \C-x\C-r
1606 (
1607 "_most_recent_file",
1608 "complete-word",
1609 "\u{18}m",
1610 "_most_recent_file",
1611 ), // _most_recent_file:1 \C-xm
1612 ("_next_tags", "list-choices", "\u{18}n", "_next_tags"), // _next_tags:1 \C-xn
1613 ("_expand_word", "complete-word", "\u{18}e", "_expand_word"), // _expand_word:1 -K (1st pair)
1614 (
1615 "_list_expansions",
1616 "list-choices",
1617 "\u{18}d",
1618 "_expand_word",
1619 ), // _expand_word:1 -K (2nd pair)
1620 (
1621 "_bash_complete-word",
1622 "complete-word",
1623 "\u{1b}~",
1624 "_bash_completions",
1625 ), // _bash_completions:1 -K
1626 (
1627 "_bash_list-choices",
1628 "list-choices",
1629 "\u{18}~",
1630 "_bash_completions",
1631 ), // _bash_completions:1 -K
1632 (
1633 "_history-complete-older",
1634 "complete-word",
1635 "\u{1b}/",
1636 "_history_complete_word",
1637 ), // _history_complete_word:1 -K
1638 (
1639 "_history-complete-newer",
1640 "complete-word",
1641 "\u{1b},",
1642 "_history_complete_word",
1643 ), // _history_complete_word:1 -K
1644 ("_expand_alias", "complete-word", "\u{18}a", "_expand_alias"), // Base/Completer/_expand_alias:1 -K
1645];
1646
1647/// Install the stock `#compdef -k`/`-K` bindings (see
1648/// STANDARD_COMP_KEYBINDINGS). Runs on the main thread next to
1649/// install_standard_complete_widgets.
1650pub fn install_standard_comp_keybindings() {
1651 for (widget, style, key, func) in STANDARD_COMP_KEYBINDINGS {
1652 install_comp_keybinding(widget, style, key, func);
1653 }
1654}
1655
1656pub fn compinit(fpath: &[PathBuf]) -> CompInitResult {
1657 let start = Instant::now();
1658
1659 // sh:129-134 — install `$_comp_dumpfile` default so engine code
1660 // that calls `getsparam("_comp_dumpfile")` sees a sensible
1661 // path. User-supplied `compinit -d FILE` overrides.
1662 if crate::ported::params::getsparam("_comp_dumpfile")
1663 .map(|s| s.is_empty())
1664 .unwrap_or(true)
1665 {
1666 let _ = crate::ported::params::setsparam(
1667 "_comp_dumpfile",
1668 &default_dumpfile_path().to_string_lossy(),
1669 );
1670 }
1671
1672 // sh:138-172 — publish `_comp_options` to the shell-side param
1673 // table so `$_comp_setup` eval at every entry point picks it
1674 // up. Use the canonical const list.
1675 crate::ported::params::setaparam(
1676 "_comp_options",
1677 COMP_OPTIONS.iter().map(|s| s.to_string()).collect(),
1678 );
1679
1680 // sh:180-190 — publish `_comp_setup` (the eval string).
1681 let _ = crate::ported::params::setsparam("_comp_setup", COMP_SETUP_EVAL);
1682
1683 // sh:195-197 — initialize the pre/post-hook arrays.
1684 init_comp_funcs_arrays();
1685
1686 // Parallel scan of all directories. Files are read concurrently but the
1687 // RESULT keeps `$fpath` order (rayon's collect is order-preserving), which
1688 // the dedup below depends on.
1689 let scanned: Vec<CompFile> = fpath
1690 .par_iter()
1691 .filter(|dir| dir.as_os_str() != "." && dir.exists())
1692 .flat_map(|dir| scan_directory(dir))
1693 .collect();
1694
1695 // sh:509 — `(( $+_i_test[$_i_name] … )) && continue`: the FIRST `$fpath`
1696 // directory holding a given completer filename wins; later copies are
1697 // skipped. The previous code ran this dedup INSIDE the parallel filter
1698 // against a shared `seen` set, so the surviving copy was whichever thread
1699 // won the race — not the first in fpath order. Doing it here, over the
1700 // already-ordered vector, makes the winner deterministic and correct.
1701 let mut seen: HashSet<String> = HashSet::new();
1702 let all_files: Vec<CompFile> = scanned
1703 .into_iter()
1704 .filter(|f| seen.insert(f.name.clone()))
1705 .collect();
1706
1707 let files_scanned = all_files.len();
1708 let dirs_scanned = fpath.len();
1709
1710 // Build the result maps
1711 let mut result = CompInitResult {
1712 scan_time_ms: start.elapsed().as_millis() as u64,
1713 dirs_scanned,
1714 files_scanned,
1715 ..Default::default()
1716 };
1717
1718 for file in &all_files {
1719 match &file.def {
1720 CompFileDef::CompDef(compdef) => {
1721 match compdef {
1722 CompDef::Commands(cmds) => {
1723 for cmd in cmds {
1724 // sh:519 — compinit registers every scanned file with
1725 // `compdef -na`, and `-n` (new) means sh:393
1726 // `if [[ -z "$new" || -z "${_comps[$1]}" ]]` — an
1727 // EXISTING entry is kept, so the first `$fpath`
1728 // directory to claim a command owns it. zshrs inserted
1729 // unconditionally (last writer won), so with two files
1730 // claiming the same command (`_df` at fpath[24] has
1731 // `#compdef df gdf`; zsh-more-completions'
1732 // `_dwarffortress` at fpath[42] has `#compdef
1733 // dwarffortress df`) `df` completed as Dwarf Fortress
1734 // and `df -<TAB>` produced nothing.
1735 //
1736 // Handle service syntax: cmd=service
1737 if let Some(eq_pos) = cmd.find('=') {
1738 let cmd_name = &cmd[..eq_pos];
1739 let service = &cmd[eq_pos + 1..];
1740 if !result.comps.contains_key(cmd_name) {
1741 result.comps.insert(cmd_name.to_string(), file.name.clone());
1742 // sh:395 — `_services[$cmd]` is set inside the
1743 // same guard, never on its own.
1744 result
1745 .services
1746 .insert(cmd_name.to_string(), service.to_string());
1747 }
1748 } else if !result.comps.contains_key(cmd) {
1749 result.comps.insert(cmd.clone(), file.name.clone());
1750 }
1751 }
1752 }
1753 CompDef::Pattern(pats) => {
1754 // c:compinit sh:396 — `_patcomps[$1]="$func"`. Pattern
1755 // compdefs go to `_patcomps` ONLY, never `_comps`.
1756 for pat in pats {
1757 result.patcomps.insert(pat.clone(), file.name.clone());
1758 }
1759 }
1760 CompDef::PostPattern(pats) => {
1761 // c:compinit sh:403 — `_postpatcomps[$1]="$func"`.
1762 for pat in pats {
1763 result.postpatcomps.insert(pat.clone(), file.name.clone());
1764 }
1765 }
1766 // One header feeding several tables (sh:384-420). Each
1767 // list lands in exactly the table its `-N`/`-p`/`-P`
1768 // prefix selected; the command half keeps the same
1769 // first-claim-wins + `cmd=service` handling as above.
1770 CompDef::Mixed {
1771 commands,
1772 patterns,
1773 postpatterns,
1774 } => {
1775 for cmd in commands {
1776 if let Some(eq_pos) = cmd.find('=') {
1777 let cmd_name = &cmd[..eq_pos];
1778 let service = &cmd[eq_pos + 1..];
1779 if !result.comps.contains_key(cmd_name) {
1780 result.comps.insert(cmd_name.to_string(), file.name.clone());
1781 result
1782 .services
1783 .insert(cmd_name.to_string(), service.to_string());
1784 }
1785 } else if !result.comps.contains_key(cmd) {
1786 result.comps.insert(cmd.clone(), file.name.clone());
1787 }
1788 }
1789 for pat in patterns {
1790 result.patcomps.insert(pat.clone(), file.name.clone());
1791 }
1792 for pat in postpatterns {
1793 result.postpatcomps.insert(pat.clone(), file.name.clone());
1794 }
1795 }
1796 CompDef::KeyBinding { style, keys } => {
1797 // sh:356-379 — `#compdef -k <style> <keys…>`: the widget
1798 // takes the FILE's name (e.g. _complete_debug). Collected
1799 // here; zle -C + bindkey happen in apply_keybindings
1800 // (this scan may run on a background thread).
1801 result
1802 .keybindings
1803 .push((file.name.clone(), style.clone(), keys.clone()));
1804 }
1805 CompDef::WidgetKey { widget, style, key } => {
1806 // sh:336-354 — `#compdef -K <widget> <style> <key>`:
1807 // the completion FUNCTION is the file's name, the
1808 // widget name is explicit.
1809 result.widgetkeys.push((
1810 widget.clone(),
1811 style.clone(),
1812 key.clone(),
1813 file.name.clone(),
1814 ));
1815 }
1816 }
1817 }
1818 CompFileDef::Autoload(opts) => {
1819 let opts_str = opts.join(" ");
1820 result.compautos.insert(file.name.clone(), opts_str);
1821 }
1822 CompFileDef::None => {}
1823 }
1824 }
1825
1826 result.files = all_files;
1827
1828 // sh:553-569 — the standard completion-widget rebind (`zle -C
1829 // complete-word .complete-word _main_complete` × 8 + conditional
1830 // menu-select + TAB/_expand rebind) is NOT done here. `compinit`
1831 // ships this fpath scan to a worker-pool thread (see
1832 // `ext_builtins::builtin_compinit`), and ZLE keymaps/widgets live
1833 // on the main thread — a `zle -C` issued from a worker never
1834 // reaches the interactive keymap, so TAB stays bound to the
1835 // builtin `expand-or-complete` and `_main_complete` never fires.
1836 // The rebind is instead performed synchronously on the main
1837 // thread in `builtin_compinit`, where it belongs; it needs only
1838 // `_main_complete` (a Rust fn, always present), not the scan
1839 // results, so deferring it to the background is unnecessary.
1840
1841 // Publish the result-side compdef state so downstream `getaparam(
1842 // "_comps")` etc. queries reflect the scan. This is the new-
1843 // compinit-finish complement of `compdef()`'s per-call publish.
1844 with_state(|s| {
1845 for (k, v) in &result.comps {
1846 s.comps.insert(k.clone(), v.clone());
1847 }
1848 for (k, v) in &result.services {
1849 s.services.insert(k.clone(), v.clone());
1850 }
1851 for (k, v) in &result.patcomps {
1852 s.patcomps.insert(k.clone(), v.clone());
1853 }
1854 for (k, v) in &result.postpatcomps {
1855 s.postpatcomps.insert(k.clone(), v.clone());
1856 }
1857 for (k, v) in &result.compautos {
1858 s.compautos.insert(k.clone(), v.clone());
1859 }
1860 publish_compdef_state_mut(s);
1861 });
1862
1863 result
1864}
1865
1866// `compdump`, `check_dump`, and `escape_zsh_string` moved to
1867// `compsys/ported/compdump.rs` (1:1 with upstream `Completion/compdump`).
1868pub use super::compdump::{check_dump, compdump};
1869
1870/// Build SQLite cache from fpath scan
1871///
1872/// This is the main entry point for initializing the completion system.
1873/// It scans fpath directories, parses #compdef directives, and populates
1874/// the SQLite cache for fast lookups.
1875pub fn build_cache_from_fpath(
1876 fpath: &[PathBuf],
1877 cache: &mut crate::compsys::cache::CompsysCache,
1878) -> std::io::Result<CompInitResult> {
1879 use std::time::Instant;
1880
1881 let t0 = Instant::now();
1882 let result = compinit(fpath);
1883 let scan_time = t0.elapsed();
1884
1885 let t1 = Instant::now();
1886
1887 // Populate comps table (_comps hash)
1888 let comps: Vec<(String, String)> = result
1889 .comps
1890 .iter()
1891 .map(|(k, v)| (k.clone(), v.clone()))
1892 .collect();
1893 cache
1894 .set_comps_bulk(&comps)
1895 .map_err(|e| std::io::Error::other(e.to_string()))?;
1896
1897 // Populate services table (_services hash)
1898 let services: Vec<(String, String)> = result
1899 .services
1900 .iter()
1901 .map(|(k, v)| (k.clone(), v.clone()))
1902 .collect();
1903 cache
1904 .set_services_bulk(&services)
1905 .map_err(|e| std::io::Error::other(e.to_string()))?;
1906
1907 // Populate patcomps table (_patcomps hash)
1908 for (pattern, function) in &result.patcomps {
1909 cache
1910 .set_patcomp(pattern, function)
1911 .map_err(|e| std::io::Error::other(e.to_string()))?;
1912 }
1913
1914 // Populate postpatcomps table (_postpatcomps hash). These are the
1915 // `#compdef -P pat` entries (compinit sh:404). They must NOT go into
1916 // `patcomps`: `_dispatch` walks `_patcomps` before the `$_comps` name
1917 // lookup (sh:26) and `_postpatcomps` after it (sh:71), and only the
1918 // post pass sets `_compskip=default` (sh:72), which is what suppresses
1919 // the sh:84 default-completer fallback. Merging the two tables ran
1920 // every `-P` completer in the wrong phase without that flag, so
1921 // `PATH=/usr/bin:<TAB>` ran `_dir_list` and then ALSO fell through to
1922 // `_value` -> `_default`, listing every file instead of directories.
1923 for (pattern, function) in &result.postpatcomps {
1924 cache
1925 .set_postpatcomp(pattern, function)
1926 .map_err(|e| std::io::Error::other(e.to_string()))?;
1927 }
1928
1929 let comps_time = t1.elapsed();
1930 let t2 = Instant::now();
1931
1932 // Populate autoloads table with function bodies for instant loading
1933 // Bodies were already read during parallel scan - no extra I/O here
1934 let autoloads: Vec<(String, String, String)> = result
1935 .files
1936 .iter()
1937 .filter(|f| matches!(f.def, CompFileDef::CompDef(_) | CompFileDef::Autoload(_)))
1938 .filter_map(|f| {
1939 let path_str = f.path.to_string_lossy().to_string();
1940 let body = f.body.as_ref()?.clone();
1941 Some((f.name.clone(), path_str, body))
1942 })
1943 .collect();
1944 cache
1945 .add_autoloads_with_bodies_bulk(&autoloads)
1946 .map_err(|e| std::io::Error::other(e.to_string()))?;
1947
1948 let autoloads_time = t2.elapsed();
1949
1950 // Timing logged by caller in vm_helper via tracing
1951
1952 Ok(result)
1953}
1954
1955/// Load _comps from existing cache (instantaneous)
1956///
1957/// Returns a CompInitResult populated from the SQLite cache without rescanning fpath.
1958/// Use this after the cache has been built with `build_cache_from_fpath`.
1959///
1960/// This is the equivalent of `compinit -C` with a valid zcompdump - it skips
1961/// the fpath scan entirely and just loads from cache.
1962#[allow(clippy::field_reassign_with_default)] // result is mutated across many subsequent statements; struct-literal init not practical
1963pub fn load_from_cache(
1964 cache: &crate::compsys::cache::CompsysCache,
1965) -> std::io::Result<CompInitResult> {
1966 use std::time::Instant;
1967 let start = Instant::now();
1968
1969 let mut result = CompInitResult::default();
1970
1971 // Load comps - single query
1972 result.comps = cache
1973 .get_all_comps()
1974 .map_err(|e| std::io::Error::other(e.to_string()))?;
1975
1976 // Load patcomps - single query
1977 for (pat, func) in cache
1978 .patcomps_kv()
1979 .map_err(|e| std::io::Error::other(e.to_string()))?
1980 {
1981 result.patcomps.insert(pat, func);
1982 }
1983
1984 // Load postpatcomps - single query. Without this the `-C` cache-hit path
1985 // published an EMPTY `_postpatcomps` (ext_builtins.rs, sh:116/121), so
1986 // `_dispatch`'s post pass had nothing to walk and every `#compdef -P`
1987 // completer (`_dir_list`, `_urls`, `_locales`, `_gcc`, `_python`, …) was
1988 // dead on any shell that started from the cache.
1989 for (pat, func) in cache
1990 .postpatcomps_kv()
1991 .map_err(|e| std::io::Error::other(e.to_string()))?
1992 {
1993 result.postpatcomps.insert(pat, func);
1994 }
1995
1996 // Services are loaded on-demand via cache.get_service() - no need to preload
1997 // This matches zsh behavior where $_services is lazily populated
1998
1999 result.scan_time_ms = start.elapsed().as_millis() as u64;
2000 result.files_scanned = result.comps.len();
2001
2002 Ok(result)
2003}
2004
2005/// Fast check if compinit is needed
2006///
2007/// Returns the number of completion entries in cache, or 0 if cache is empty/invalid.
2008/// Use this to decide whether to run full compinit or load_from_cache.
2009pub fn cache_entry_count(cache: &crate::compsys::cache::CompsysCache) -> usize {
2010 cache.comp_count().unwrap_or(0) as usize
2011}
2012
2013/// Lazy compinit - validates cache exists but doesn't load into memory
2014///
2015/// This is the fastest option for shell startup. It just verifies the cache
2016/// is valid and returns immediately. Actual lookups happen via cache.get_comp().
2017///
2018/// Returns (is_valid, entry_count) in microseconds.
2019pub fn compinit_lazy(cache: &crate::compsys::cache::CompsysCache) -> (bool, usize) {
2020 let count = cache.comp_count().unwrap_or(0) as usize;
2021 (count > 0, count)
2022}
2023
2024/// Metadata key holding the `comps` row count a cache build finished
2025/// with. Written as the LAST statement of `build_cache_from_fpath`'s
2026/// caller, after every table is populated — see
2027/// `stamp_cache_complete` and `cache_is_valid`.
2028pub const CACHE_COMPLETE_KEY: &str = "comps_rows_at_build_end";
2029
2030/// Metadata key holding `(mtime_secs, mtime_nsecs, len)` of the `zshrs`
2031/// binary that built the cache — see
2032/// `crate::compsys::cache::binary_identity_stamp`.
2033///
2034/// The row-count stamp above says a build FINISHED; it says nothing
2035/// about WHICH build. The rows carry completer names, service names and
2036/// pattern splits whose meaning is fixed by the code that wrote them,
2037/// and the only guard against a cache outliving that code was a
2038/// hand-bumped `COMPLETION_SCHEMA_GENERATION` constant, i.e. a human
2039/// remembering. Stamping the producing binary makes every rebuilt
2040/// `zshrs` reject its predecessor's rows without anyone remembering
2041/// anything.
2042pub const CACHE_BINARY_KEY: &str = "builder_binary_identity";
2043
2044/// Record that a freshly built cache is complete.
2045///
2046/// Must be the final write of a build. Pairs with `cache_is_valid`.
2047pub fn stamp_cache_complete(cache: &crate::compsys::cache::CompsysCache) -> bool {
2048 let Some(binary) = crate::compsys::cache::binary_identity_stamp() else {
2049 // No `current_exe()`: the cache cannot be attributed to a build,
2050 // and an unattributable cache is never installed.
2051 return false;
2052 };
2053 if cache.set_metadata(CACHE_BINARY_KEY, &binary).is_err() {
2054 return false;
2055 }
2056 match cache.comp_count() {
2057 Ok(n) => cache
2058 .set_metadata(CACHE_COMPLETE_KEY, &n.to_string())
2059 .is_ok(),
2060 Err(_) => false,
2061 }
2062}
2063
2064/// Check if cache is valid and up-to-date
2065///
2066/// Returns true only for a cache some build finished writing; false when
2067/// it is empty, unstamped, or still filling.
2068///
2069/// A bare `comp_count() > 0` was NOT a validity test. The rebuild path
2070/// spends seconds inserting ~50k `comps` rows, and up to 16 of the user's
2071/// shells run against this one file, so a shell starting inside that
2072/// window saw a non-zero partial count, took the cache-hit branch, and
2073/// published a `_comps` holding a fraction of the registrations —
2074/// `_dispatch` then resolved no completer for any command and every
2075/// `<cmd> <TAB>` completed nothing. Matching the stamped count against
2076/// the live count makes "still filling" and "builder died midway" both
2077/// fail: the stamp is written once, at the end, and any later insert
2078/// moves the live count away from it.
2079pub fn cache_is_valid(cache: &crate::compsys::cache::CompsysCache) -> bool {
2080 let rows = cache.comp_count().unwrap_or(0);
2081 if rows <= 0 {
2082 return false;
2083 }
2084 // Built by THIS binary, exactly. A cache written by another build
2085 // is rejected whether it is older or newer, so a downgrade is a
2086 // miss rather than a silent hit on rows the running code no longer
2087 // agrees with.
2088 let Some(running) = crate::compsys::cache::binary_identity_stamp() else {
2089 return false;
2090 };
2091 match cache.get_metadata(CACHE_BINARY_KEY) {
2092 Ok(Some(built_by)) if built_by == running => {}
2093 _ => return false,
2094 }
2095 match cache.get_metadata(CACHE_COMPLETE_KEY) {
2096 Ok(Some(stamp)) => stamp.parse::<i64>().map(|n| n == rows).unwrap_or(false),
2097 _ => false,
2098 }
2099}
2100
2101/// Get system fpath from environment or defaults
2102pub fn get_system_fpath() -> Vec<PathBuf> {
2103 // Try FPATH env var first
2104 if let Ok(fpath_str) = std::env::var("FPATH") {
2105 if !fpath_str.is_empty() {
2106 return fpath_str.split(':').map(PathBuf::from).collect();
2107 }
2108 }
2109
2110 // Default paths for common systems
2111 let mut paths = Vec::new();
2112
2113 // macOS Homebrew
2114 for base in &["/opt/homebrew", "/usr/local"] {
2115 paths.push(PathBuf::from(format!("{}/share/zsh/site-functions", base)));
2116 paths.push(PathBuf::from(format!("{}/share/zsh/functions", base)));
2117 }
2118
2119 // System zsh
2120 for version in &["5.9", "5.8", "5.7"] {
2121 paths.push(PathBuf::from(format!(
2122 "/usr/share/zsh/{}/functions",
2123 version
2124 )));
2125 }
2126 paths.push(PathBuf::from("/usr/share/zsh/functions"));
2127 paths.push(PathBuf::from("/usr/share/zsh/site-functions"));
2128
2129 // Zinit/zplugin common paths
2130 if let Ok(home) = std::env::var("HOME") {
2131 paths.push(PathBuf::from(format!("{}/.zinit/completions", home)));
2132 paths.push(PathBuf::from(format!("{}/.zplugin/completions", home)));
2133 paths.push(PathBuf::from(format!(
2134 "{}/.local/share/zsh/site-functions",
2135 home
2136 )));
2137 }
2138
2139 // Filter to existing directories
2140 paths.into_iter().filter(|p| p.exists()).collect()
2141}
2142
2143/// Options for compinit
2144#[derive(Clone, Debug, Default)]
2145pub struct CompInitOpts {
2146 /// Dump file path (-d)
2147 pub dump_file: Option<PathBuf>,
2148 /// Skip dump (-D)
2149 pub no_dump: bool,
2150 /// Skip security check (-C)
2151 pub no_check: bool,
2152 /// Ignore insecure dirs (-i)
2153 pub ignore_insecure: bool,
2154 /// Use insecure dirs (-u)
2155 pub use_insecure: bool,
2156}
2157
2158impl CompInitOpts {
2159 /// Parse compinit arguments
2160 pub fn parse(args: &[String]) -> Self {
2161 let mut opts = Self::default();
2162 let mut i = 0;
2163
2164 while i < args.len() {
2165 match args[i].as_str() {
2166 "-d" if i + 1 < args.len() && !args[i + 1].starts_with('-') => {
2167 opts.dump_file = Some(PathBuf::from(&args[i + 1]));
2168 i += 1;
2169 }
2170 "-D" => opts.no_dump = true,
2171 "-C" => opts.no_check = true,
2172 "-i" => opts.ignore_insecure = true,
2173 "-u" => opts.use_insecure = true,
2174 _ => {}
2175 }
2176 i += 1;
2177 }
2178
2179 opts
2180 }
2181}
2182
2183// =====================================================================
2184// compdef() — runtime registration entry point.
2185//
2186// Upstream defines this inside `Completion/compinit` (sh:253-446); we
2187// mirror that organizational choice. User `.zshrc` lines like:
2188// compdef _git git
2189// compdef -p '*-test' _test
2190// compdef -d obsolete
2191// land here.
2192//
2193// State lives in a session-side `CompdefState` (a Mutex<HashMap>
2194// quintet for `_comps`, `_services`, `_patcomps`, `_postpatcomps`,
2195// `_compautos`). Cluster ports already read these via shell-side
2196// `assoc_get("_comps")` etc.; the published-to-paramtab step happens
2197// in `publish_compdef_state_mut` at the bottom of this section, which
2198// MERGES into those parameters rather than rebuilding them (see
2199// `merge_hparam`) — the state is one contributor to `_comps`, never
2200// its definition.
2201// =====================================================================
2202
2203/// Session-side compdef registrations. Mirrors the five upstream
2204/// assoc arrays one-for-one.
2205#[derive(Default)]
2206pub struct CompdefState {
2207 pub comps: HashMap<String, String>,
2208 pub services: HashMap<String, String>,
2209 pub patcomps: HashMap<String, String>,
2210 pub postpatcomps: HashMap<String, String>,
2211 pub compautos: HashMap<String, String>,
2212 /// Keys a `compdef -d` (sh:426-444 `unset "_comps[$^@]"`) removed and
2213 /// that the next publish still has to unset on the shell side.
2214 /// Removal is the one edit a merge cannot express, so it is carried
2215 /// explicitly instead of being implied by the absence of a key.
2216 removed: CompdefRemovals,
2217}
2218
2219/// Per-array key lists for pending `compdef -d` removals.
2220#[derive(Default)]
2221struct CompdefRemovals {
2222 comps: Vec<String>,
2223 services: Vec<String>,
2224 patcomps: Vec<String>,
2225 postpatcomps: Vec<String>,
2226}
2227
2228static COMPDEF_STATE: Mutex<Option<CompdefState>> = Mutex::new(None);
2229
2230/// Depth of the enclosing `compdef_batch` calls. See that function.
2231static PUBLISH_DEPTH: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
2232
2233/// Lock the session-side state, initializing on first call.
2234fn with_state<F, R>(f: F) -> R
2235where
2236 F: FnOnce(&mut CompdefState) -> R,
2237{
2238 let mut guard = COMPDEF_STATE.lock().unwrap();
2239 if guard.is_none() {
2240 *guard = Some(CompdefState::default());
2241 }
2242 f(guard.as_mut().unwrap())
2243}
2244
2245/// Run `f` with shell-side publication held until it returns, then
2246/// publish once.
2247///
2248/// Publication is a whole-hash read-modify-write: `gethparam`/`sethparam`
2249/// are the only associative-array accessors `params.rs` exposes, so there
2250/// is no way to assign a single `_comps[$cmd]` the way sh:376 does. A bulk
2251/// replay (`cdreplay` after zinit turbo) calls `compdef` once per deferred
2252/// registration and would otherwise pay that read-modify-write over a
2253/// 50k-entry `_comps` on every one of them. The batched end state is
2254/// identical — the same accumulated `CompdefState`, published once.
2255pub fn compdef_batch<R>(f: impl FnOnce() -> R) -> R {
2256 use std::sync::atomic::Ordering;
2257 // Restores the depth even if `f` panics or returns early.
2258 struct Depth;
2259 impl Drop for Depth {
2260 fn drop(&mut self) {
2261 PUBLISH_DEPTH.fetch_sub(1, std::sync::atomic::Ordering::Relaxed);
2262 }
2263 }
2264 PUBLISH_DEPTH.fetch_add(1, Ordering::Relaxed);
2265 let out = {
2266 let _depth = Depth;
2267 f()
2268 };
2269 with_state(publish_compdef_state_mut);
2270 out
2271}
2272
2273/// Apply one array's pending edits to its shell-side associative array.
2274///
2275/// `set` is overlaid onto whatever the parameter already holds and
2276/// `remove` is unset from it — the parameter is never rebuilt from `set`
2277/// alone. That distinction is the whole fix: `CompdefState` holds only
2278/// what THIS process's `compdef` calls (and its own `$fpath` scan)
2279/// registered, while `compinit -C`'s cache-hit path fills the parameter
2280/// directly (`ext_builtins.rs`, `set_assoc`) without going through the
2281/// state at all. Publishing `flatten(&s.comps)` wholesale therefore
2282/// replaced a 51 647-entry `_comps` with however many keys the state
2283/// happened to have — one, for a session whose only `compdef` was
2284/// `_zstyle zstyle` — and `_dispatch` then resolved an empty completer
2285/// for EVERY command, so `man <TAB>`, `git <TAB>` and `kill <TAB>` all
2286/// completed nothing at all.
2287fn merge_hparam(name: &str, set: &HashMap<String, String>, remove: &[String]) {
2288 if set.is_empty() && remove.is_empty() {
2289 return;
2290 }
2291 // BTreeMap so the published pair order is deterministic, matching what
2292 // the previous sort-by-key flatten produced. The read goes through
2293 // `subst::assoc_get` because that is the accessor backed by the same
2294 // `paramtab_hashed_storage` `sethparam` writes — `gethparam` returns
2295 // the VALUES only (params.rs:5723-5728), not the key/value pairs.
2296 let mut merged: std::collections::BTreeMap<String, String> = std::collections::BTreeMap::new();
2297 if let Some(existing) = crate::ported::subst::assoc_get(name) {
2298 for (k, v) in existing {
2299 merged.insert(k, v);
2300 }
2301 }
2302 for k in remove {
2303 merged.remove(k);
2304 }
2305 for (k, v) in set {
2306 merged.insert(k.clone(), v.clone());
2307 }
2308 let mut out = Vec::with_capacity(merged.len() * 2);
2309 for (k, v) in merged {
2310 out.push(k);
2311 out.push(v);
2312 }
2313 // These are ASSOCIATIVE arrays in zsh (`typeset -gHA _comps _services
2314 // _patcomps _postpatcomps` / `_compautos`, sh:116/121). They MUST be
2315 // published via `sethparam` (hashed) — the `[k, v, k, v, …]` pair
2316 // layout above is what `sethparam` consumes. Using `setaparam` (plain
2317 // array) made `_comps` a flat array, so `${_comps[ls]}` couldn't
2318 // hash-lookup and `_complete`/`_dispatch` found no completer for ANY
2319 // command → every `<cmd> <TAB>` (incl. `ls -<TAB>`) just rang the bell.
2320 // The synchronous `compinit -C` path used `set_assoc` (→ sethparam) and
2321 // worked, which is why only the fresh/compdef-driven path was broken.
2322 // Bug #655.
2323 crate::ported::params::sethparam(name, out);
2324}
2325
2326/// Publish the in-memory state into the shell-side assoc arrays so
2327/// engine cluster code (which reads via `getaparam("_comps")` etc.)
2328/// sees the updates.
2329///
2330/// A no-op inside `compdef_batch`; the batch publishes on exit.
2331fn publish_compdef_state_mut(s: &mut CompdefState) {
2332 use std::sync::atomic::Ordering;
2333 if PUBLISH_DEPTH.load(Ordering::Relaxed) > 0 {
2334 return;
2335 }
2336 merge_hparam("_comps", &s.comps, &s.removed.comps);
2337 merge_hparam("_services", &s.services, &s.removed.services);
2338 merge_hparam("_patcomps", &s.patcomps, &s.removed.patcomps);
2339 merge_hparam("_postpatcomps", &s.postpatcomps, &s.removed.postpatcomps);
2340 merge_hparam("_compautos", &s.compautos, &[]);
2341 s.removed = CompdefRemovals::default();
2342}
2343
2344/// Read one key out of a shell-side assoc array.
2345///
2346/// The parameter — not `CompdefState` — is the source of truth for what
2347/// is registered: `compinit -C` fills it from the dump/cache without
2348/// touching the state (see `merge_hparam`).
2349fn hparam_has_key(name: &str, key: &str) -> bool {
2350 crate::ported::subst::assoc_get(name)
2351 .map(|m| m.contains_key(key))
2352 .unwrap_or(false)
2353}
2354
2355/// Parse `compdef`'s short-option flags via the upstream
2356/// `getopts "anpPkKde"` (sh:267).
2357#[derive(Default, Debug)]
2358struct CompdefFlags {
2359 autol: bool,
2360 new: bool,
2361 delete: bool,
2362 eval: bool,
2363 /// Mutually-exclusive `-p`/`-P`/`-k`/`-K`. Only the most-recent
2364 /// wins; upstream errors on duplicate, we tolerate.
2365 spec_type: SpecType,
2366}
2367
2368#[derive(Default, Debug, PartialEq, Clone, Copy)]
2369enum SpecType {
2370 #[default]
2371 Normal,
2372 Pattern,
2373 PostPattern,
2374 Key,
2375 WidgetKey,
2376}
2377
2378fn parse_compdef_flags(args: &[String]) -> Result<(CompdefFlags, usize), String> {
2379 let mut flags = CompdefFlags::default();
2380 let mut idx = 0usize;
2381 while idx < args.len() {
2382 let a = &args[idx];
2383 if !a.starts_with('-') || a == "-" || a == "--" {
2384 break;
2385 }
2386 // sh:267 getopts allows combined flags like `-an`. Walk each
2387 // letter after the leading `-`.
2388 for c in a.chars().skip(1) {
2389 match c {
2390 'a' => flags.autol = true,
2391 'n' => flags.new = true,
2392 'd' => flags.delete = true,
2393 'e' => flags.eval = true,
2394 'p' => flags.spec_type = SpecType::Pattern,
2395 'P' => flags.spec_type = SpecType::PostPattern,
2396 'k' => flags.spec_type = SpecType::Key,
2397 'K' => flags.spec_type = SpecType::WidgetKey,
2398 _ => return Err(format!("compdef: unknown option: -{}", c)),
2399 }
2400 }
2401 idx += 1;
2402 }
2403 Ok((flags, idx))
2404}
2405
2406/// `compdef` — register or unregister completion functions for
2407/// commands. Faithful to upstream `Completion/compinit` sh:253-446.
2408///
2409/// Returns the upstream-compatible exit code: 0 on success, 1 on
2410/// usage error.
2411pub fn compdef(args: &[String]) -> i32 {
2412 // sh:262
2413 if args.is_empty() {
2414 eprintln!("compdef: I need arguments");
2415 return 1;
2416 }
2417 let (flags, mut idx) = match parse_compdef_flags(args) {
2418 Ok(p) => p,
2419 Err(e) => {
2420 eprintln!("{}", e);
2421 return 1;
2422 }
2423 };
2424 // sh:293
2425 if idx >= args.len() {
2426 eprintln!("compdef: I need arguments");
2427 return 1;
2428 }
2429
2430 if flags.delete {
2431 // sh:426-444 -d: delete by name from the right hash. The key has
2432 // to be dropped from the session state AND queued for removal from
2433 // the shell parameter — a key registered by `compinit -C`'s cache
2434 // load is only ever in the parameter, so removing it from the state
2435 // alone would leave `compdef -d` a no-op for everything the dump
2436 // defined.
2437 let names = &args[idx..];
2438 with_state(|s| match flags.spec_type {
2439 SpecType::Pattern => {
2440 for n in names {
2441 s.patcomps.remove(n);
2442 s.removed.patcomps.push(n.clone());
2443 }
2444 }
2445 SpecType::PostPattern => {
2446 for n in names {
2447 s.postpatcomps.remove(n);
2448 s.removed.postpatcomps.push(n.clone());
2449 }
2450 }
2451 SpecType::Key | SpecType::WidgetKey => {
2452 eprintln!("compdef: cannot restore key bindings");
2453 }
2454 SpecType::Normal => {
2455 for n in names {
2456 s.comps.remove(n);
2457 s.services.remove(n);
2458 s.removed.comps.push(n.clone());
2459 s.removed.services.push(n.clone());
2460 }
2461 }
2462 });
2463 with_state(publish_compdef_state_mut);
2464 return 0;
2465 }
2466
2467 // sh:298-327 service-alias mode: no flags + first arg contains `=`.
2468 // Each subsequent arg must also contain `=` (else it's an error).
2469 if !flags.eval && args[idx].contains('=') {
2470 let mut ret: i32 = 0;
2471 while idx < args.len() {
2472 let entry = args[idx].clone();
2473 idx += 1;
2474 if !entry.contains('=') {
2475 eprintln!("compdef: invalid argument: {}", entry);
2476 ret = 1;
2477 continue;
2478 }
2479 let mut sp = entry.splitn(2, '=');
2480 let cmd = sp.next().unwrap_or("").to_string();
2481 let svc_in = sp.next().unwrap_or("").to_string();
2482 // sh:307-311 — resolve `$svc` and look up its completion
2483 // function. zsh reads the `_comps`/`_services` PARAMETERS
2484 // (`func="$_comps[...]"`), which are the source of truth:
2485 // the `compinit -C` dump-source path and third-party plugins
2486 // populate the parameters directly, while the internal
2487 // `s.comps` state only sees native `compdef` calls — so it
2488 // lags and made `compdef func=cmd` wrongly report
2489 // "unknown command or service" even though `$_comps[cmd]`
2490 // was set. Read the parameters first, fall back to s.comps.
2491 let comps_param = crate::ported::subst::assoc_get("_comps").unwrap_or_default();
2492 // sh:307 — `${_services[(r)$svc]:-$svc}`. `(r)` returns the
2493 // matching VALUE (== $svc for a literal), else the `:-$svc`
2494 // default, so the effective service key is `$svc` itself.
2495 let resolved_svc = svc_in.clone();
2496 let func = comps_param
2497 .get(&resolved_svc)
2498 .filter(|f| !f.is_empty())
2499 .cloned()
2500 .or_else(|| {
2501 with_state(|s| s.comps.get(&resolved_svc).cloned()).filter(|f| !f.is_empty())
2502 })
2503 .or_else(|| {
2504 // sh:311 fallback to first matching pat/postpat key,
2505 // again preferring the parameters over s.* state.
2506 let pat = crate::ported::subst::assoc_get("_patcomps").unwrap_or_default();
2507 let postpat =
2508 crate::ported::subst::assoc_get("_postpatcomps").unwrap_or_default();
2509 pat.iter()
2510 .find(|(k, _)| pattern_matches(k, &svc_in))
2511 .map(|(_, v)| v.clone())
2512 .or_else(|| {
2513 postpat
2514 .iter()
2515 .find(|(k, _)| pattern_matches(k, &svc_in))
2516 .map(|(_, v)| v.clone())
2517 })
2518 .or_else(|| {
2519 with_state(|s| {
2520 s.patcomps
2521 .iter()
2522 .find(|(k, _)| pattern_matches(k, &svc_in))
2523 .map(|(_, v)| v.clone())
2524 .or_else(|| {
2525 s.postpatcomps
2526 .iter()
2527 .find(|(k, _)| pattern_matches(k, &svc_in))
2528 .map(|(_, v)| v.clone())
2529 })
2530 })
2531 })
2532 })
2533 .unwrap_or_default();
2534 if func.is_empty() {
2535 eprintln!("compdef: unknown command or service: {}", svc_in);
2536 ret = 1;
2537 continue;
2538 }
2539 // sh:308-309 — `[[ -n ${_services[$svc]} ]] && svc=${_services[$svc]}`.
2540 let services_param = crate::ported::subst::assoc_get("_services").unwrap_or_default();
2541 let svc_for_state = services_param
2542 .get(&svc_in)
2543 .filter(|v| !v.is_empty())
2544 .cloned()
2545 .or_else(|| with_state(|s| s.services.get(&svc_in).cloned()))
2546 .unwrap_or(svc_in.clone());
2547 with_state(|s| {
2548 s.comps.insert(cmd.clone(), func.clone());
2549 s.services.insert(cmd, svc_for_state);
2550 });
2551 }
2552 with_state(publish_compdef_state_mut);
2553 return ret;
2554 }
2555
2556 // sh:332-334 First positional after flags is the function name.
2557 let func = args[idx].clone();
2558 idx += 1;
2559
2560 // sh:333 `-a` → autoload
2561 if flags.autol && func.starts_with('_') {
2562 // dispatch `autoload -rUz <func>` via the exec accessors bridge.
2563 let _ = crate::ported::exec::dispatch_function_call(
2564 "autoload",
2565 &["-rUz".to_string(), func.clone()],
2566 );
2567 // Track for the dump file
2568 with_state(|s| {
2569 s.compautos.insert(func.clone(), "-rUz".to_string());
2570 });
2571 }
2572
2573 // sh:336-425
2574 match flags.spec_type {
2575 SpecType::WidgetKey => {
2576 // sh:337-355 -K widget-name comp-widget key (in triples)
2577 let mut i = idx;
2578 while i + 2 < args.len() {
2579 let mut wname = args[i].clone();
2580 let mut comp_widget = args[i + 1].clone();
2581 let key = args[i + 2].clone();
2582 if !wname.starts_with('_') {
2583 wname = format!("_{}", wname);
2584 }
2585 if !comp_widget.starts_with('.') {
2586 comp_widget = format!(".{}", comp_widget);
2587 }
2588 // sh:346 `zle -C` + sh:347-352 `bindkey` — through the
2589 // BUILTIN entries; dispatch_function_call resolves only
2590 // shell functions and silently no-ops for builtins, which
2591 // left every `#compdef -K` widget (^Xe _expand_word,
2592 // \e/ _history-complete-older, …) unbound.
2593 install_comp_keybinding(&wname, &comp_widget, &key, &func);
2594 i += 3;
2595 }
2596 }
2597 SpecType::Key => {
2598 // sh:356-379 -k style key... (in 1+ pairs)
2599 if idx >= args.len() {
2600 eprintln!("compdef: missing keys");
2601 return 1;
2602 }
2603 let mut style = args[idx].clone();
2604 idx += 1;
2605 if !style.starts_with('.') {
2606 style = format!(".{}", style);
2607 }
2608 // sh:365 `zle -C` + sh:373-378 `bindkey` — through the BUILTIN
2609 // entries (see the -K arm above): the dispatch_function_call
2610 // spelling silently no-op'd, so `#compdef -k` widgets — ^X?
2611 // _complete_debug, ^Xh _complete_help — never bound and the
2612 // keys self-inserted literally.
2613 for key in &args[idx..] {
2614 install_comp_keybinding(&func, &style, key, &func);
2615 }
2616 }
2617 _ => {
2618 // sh:381-424 normal / pattern / postpattern
2619 let mut effective_type = flags.spec_type;
2620 while idx < args.len() {
2621 let arg = args[idx].clone();
2622 idx += 1;
2623 // sh:385-390 inline type switch
2624 match arg.as_str() {
2625 "-N" => {
2626 effective_type = SpecType::Normal;
2627 continue;
2628 }
2629 "-p" => {
2630 effective_type = SpecType::Pattern;
2631 continue;
2632 }
2633 "-P" => {
2634 effective_type = SpecType::PostPattern;
2635 continue;
2636 }
2637 _ => {}
2638 }
2639 with_state(|s| match effective_type {
2640 SpecType::Pattern => {
2641 // sh:393-398 — `key=val` rewrites to `=val=func`
2642 if let Some(eq) = arg.find('=') {
2643 let key = arg[..eq].to_string();
2644 let val = arg[eq + 1..].to_string();
2645 s.patcomps.insert(key, format!("={}={}", val, func));
2646 } else {
2647 s.patcomps.insert(arg.clone(), func.clone());
2648 }
2649 }
2650 SpecType::PostPattern => {
2651 if let Some(eq) = arg.find('=') {
2652 let key = arg[..eq].to_string();
2653 let val = arg[eq + 1..].to_string();
2654 s.postpatcomps.insert(key, format!("={}={}", val, func));
2655 } else {
2656 s.postpatcomps.insert(arg.clone(), func.clone());
2657 }
2658 }
2659 _ => {
2660 // sh:407-419 normal: cmd or cmd=svc
2661 let (cmd, svc) = if let Some(eq) = arg.find('=') {
2662 (arg[..eq].to_string(), Some(arg[eq + 1..].to_string()))
2663 } else {
2664 (arg.clone(), None)
2665 };
2666 // sh:415 — `-n`: no-clobber. zsh tests
2667 // `[[ -z ${_comps[$1]} ]]` against the PARAMETER, so
2668 // an entry that came from the dump/cache load counts
2669 // as already-defined; testing only the session state
2670 // let every `compdef -na` from an fpath rescan
2671 // overwrite the dump's registration.
2672 if flags.new
2673 && (s.comps.contains_key(&cmd) || hparam_has_key("_comps", &cmd))
2674 {
2675 return;
2676 }
2677 s.comps.insert(cmd.clone(), func.clone());
2678 if let Some(svc) = svc {
2679 s.services.insert(cmd, svc);
2680 }
2681 }
2682 });
2683 }
2684 }
2685 }
2686 with_state(publish_compdef_state_mut);
2687 0
2688}
2689
2690/// sh:311 pattern-matching helper. Uses the real `pattern.rs`
2691/// matcher so `(K)` assoc-key glob matching is faithful.
2692fn pattern_matches(pat: &str, s: &str) -> bool {
2693 match crate::ported::pattern::patcompile(
2694 &{
2695 let mut __pat_tok = (pat).to_string();
2696 crate::ported::glob::tokenize(&mut __pat_tok);
2697 __pat_tok
2698 },
2699 0,
2700 None,
2701 ) {
2702 Some(prog) => crate::ported::pattern::pattry(&prog, s),
2703 None => pat == s,
2704 }
2705}
2706
2707/// Reset session-side state (test-only helper; exposed via
2708/// `#[cfg(test)]` users).
2709///
2710/// Clears the shell-side parameters too. Publication merges into them
2711/// (`merge_hparam`), so they outlive the `CompdefState` and would carry
2712/// one case's registrations into the next — `-n`'s no-clobber test reads
2713/// `_comps` directly, and a leftover `_comps[git]` would silently skip
2714/// the registration the next case is asserting on.
2715#[cfg(test)]
2716pub fn reset_compdef_state() {
2717 *COMPDEF_STATE.lock().unwrap() = Some(CompdefState::default());
2718 for name in [
2719 "_comps",
2720 "_services",
2721 "_patcomps",
2722 "_postpatcomps",
2723 "_compautos",
2724 ] {
2725 crate::ported::params::sethparam(name, Vec::new());
2726 }
2727}
2728
2729/// Snapshot of the session-side state — what THIS process's `compdef`
2730/// calls and `$fpath` scan registered.
2731///
2732/// Not the full `_comps`: a `compinit -C` that loaded the dump/SQLite
2733/// cache fills the shell parameter without going through this state, so
2734/// read `subst::assoc_get("_comps")` when the question is "what is
2735/// registered", and this when the question is "what did we register".
2736pub fn snapshot_compdef_state() -> CompdefState {
2737 with_state(|s| CompdefState {
2738 comps: s.comps.clone(),
2739 services: s.services.clone(),
2740 patcomps: s.patcomps.clone(),
2741 postpatcomps: s.postpatcomps.clone(),
2742 compautos: s.compautos.clone(),
2743 removed: CompdefRemovals::default(),
2744 })
2745}
2746
2747#[cfg(test)]
2748mod tests {
2749 use super::*;
2750
2751 #[test]
2752 fn test_parse_compdef_commands() {
2753 let def = parse_first_line("#compdef git svn hg");
2754 match def {
2755 CompFileDef::CompDef(CompDef::Commands(cmds)) => {
2756 assert_eq!(cmds, vec!["git", "svn", "hg"]);
2757 }
2758 _ => panic!("Expected Commands"),
2759 }
2760 }
2761
2762 #[test]
2763 fn test_parse_compdef_pattern() {
2764 let def = parse_first_line("#compdef -p 'c*'");
2765 match def {
2766 CompFileDef::CompDef(CompDef::Pattern(pats)) => {
2767 assert_eq!(pats, vec!["'c*'".to_string()]);
2768 }
2769 _ => panic!("Expected Pattern"),
2770 }
2771 }
2772
2773 // Bug #657 — `#compdef -P pat` must route to `_postpatcomps` (post-pattern),
2774 // NOT `_comps`; `#compdef -p pat` to `_patcomps` only, NOT `_comps`.
2775 #[test]
2776 fn test_parse_compdef_postpattern_routing() {
2777 match parse_first_line("#compdef -P 'pip[0-9.]#'") {
2778 CompFileDef::CompDef(CompDef::PostPattern(pats)) => {
2779 assert_eq!(pats, vec!["'pip[0-9.]#'".to_string()]);
2780 }
2781 other => panic!("Expected PostPattern, got {:?}", other),
2782 }
2783 }
2784
2785 /// sh:384-420 — `-N`/`-p`/`-P` are POSITIONAL: they re-target the words
2786 /// that follow, anywhere in the line. Reading a flag only in first
2787 /// position registered `_gcc`'s three post-patterns as literal command
2788 /// names, so `gcc-14 <TAB>` never reached `_gcc` and `$_postpatcomps`
2789 /// held 10 entries where zsh 5.9.2 holds 25.
2790 #[test]
2791 fn test_parse_compdef_trailing_flags_switch_table() {
2792 let line = "#compdef gcc g++ -value-,CFLAGS,-default- -P gcc-* -P g++-* -p early*";
2793 match parse_first_line(line) {
2794 CompFileDef::CompDef(CompDef::Mixed {
2795 commands,
2796 patterns,
2797 postpatterns,
2798 }) => {
2799 assert_eq!(commands, vec!["gcc", "g++", "-value-,CFLAGS,-default-"]);
2800 assert_eq!(patterns, vec!["early*"]);
2801 assert_eq!(postpatterns, vec!["gcc-*", "g++-*"]);
2802 }
2803 other => panic!("Expected Mixed, got {:?}", other),
2804 }
2805 // `-N` switches back to plain command names (sh:385-386).
2806 match parse_first_line("#compdef -p pat* -N cmd") {
2807 CompFileDef::CompDef(CompDef::Mixed {
2808 commands,
2809 patterns,
2810 postpatterns,
2811 }) => {
2812 assert_eq!(commands, vec!["cmd"]);
2813 assert_eq!(patterns, vec!["pat*"]);
2814 assert!(postpatterns.is_empty());
2815 }
2816 other => panic!("Expected Mixed, got {:?}", other),
2817 }
2818 // sh:534-539 — only a leading `-[pPkK](n|)` is an option. Anything
2819 // else is a positional NAME, including `-m` (which is how zsh ends
2820 // up with a command literally named `-m` from `_squishy`'s header).
2821 match parse_first_line(r#"#compdef squishy "python -m squishy""#) {
2822 CompFileDef::CompDef(CompDef::Commands(cmds)) => {
2823 assert_eq!(cmds, vec!["squishy", "\"python", "-m", "squishy\""]);
2824 }
2825 other => panic!("Expected Commands, got {:?}", other),
2826 }
2827 }
2828
2829 #[test]
2830 fn test_parse_autoload() {
2831 let def = parse_first_line("#autoload -U -z");
2832 match def {
2833 CompFileDef::Autoload(opts) => {
2834 assert_eq!(opts, vec!["-U", "-z"]);
2835 }
2836 _ => panic!("Expected Autoload"),
2837 }
2838 }
2839
2840 #[test]
2841 fn test_parse_compdef_key() {
2842 let def = parse_first_line("#compdef -k complete-word ^X^C");
2843 match def {
2844 CompFileDef::CompDef(CompDef::KeyBinding { style, keys }) => {
2845 assert_eq!(style, "complete-word");
2846 assert_eq!(keys, vec!["^X^C"]);
2847 }
2848 _ => panic!("Expected KeyBinding"),
2849 }
2850 }
2851
2852 #[test]
2853 fn test_parse_compdef_redirect_context() {
2854 // _bzip2 line: has regular commands + context entries with services
2855 let def = parse_first_line("#compdef bzip2 bunzip2 bzcat=bunzip2 bzip2recover -redirect-,<,bunzip2=bunzip2 -redirect-,>,bzip2=bunzip2 -redirect-,<,bzip2=bzip2");
2856 match def {
2857 CompFileDef::CompDef(CompDef::Commands(cmds)) => {
2858 // Should contain all entries
2859 assert!(cmds.contains(&"bzip2".to_string()), "missing bzip2");
2860 assert!(cmds.contains(&"bunzip2".to_string()), "missing bunzip2");
2861 assert!(
2862 cmds.contains(&"bzcat=bunzip2".to_string()),
2863 "missing bzcat=bunzip2"
2864 );
2865 assert!(
2866 cmds.contains(&"bzip2recover".to_string()),
2867 "missing bzip2recover"
2868 );
2869 assert!(
2870 cmds.contains(&"-redirect-,<,bunzip2=bunzip2".to_string()),
2871 "missing redirect bunzip2"
2872 );
2873 assert!(
2874 cmds.contains(&"-redirect-,>,bzip2=bunzip2".to_string()),
2875 "missing redirect >,bzip2"
2876 );
2877 assert!(
2878 cmds.contains(&"-redirect-,<,bzip2=bzip2".to_string()),
2879 "missing redirect <,bzip2"
2880 );
2881 assert_eq!(cmds.len(), 7, "cmds: {:?}", cmds);
2882 }
2883 other => panic!("Expected Commands, got {:?}", other),
2884 }
2885 }
2886
2887 #[test]
2888 fn test_parse_compdef_context_entries() {
2889 // -default- style entries
2890 let def = parse_first_line("#compdef -default-");
2891 match def {
2892 CompFileDef::CompDef(CompDef::Commands(cmds)) => {
2893 assert_eq!(cmds, vec!["-default-"]);
2894 }
2895 other => panic!("Expected Commands, got {:?}", other),
2896 }
2897
2898 // bare hyphen + commands
2899 let def = parse_first_line("#compdef - nohup eval time");
2900 match def {
2901 CompFileDef::CompDef(CompDef::Commands(cmds)) => {
2902 assert!(cmds.contains(&"-".to_string()));
2903 assert!(cmds.contains(&"nohup".to_string()));
2904 assert!(cmds.contains(&"eval".to_string()));
2905 assert!(cmds.contains(&"time".to_string()));
2906 }
2907 other => panic!("Expected Commands, got {:?}", other),
2908 }
2909
2910 // -value- entries
2911 let def = parse_first_line("#compdef -value- -array-value- -value-,-default-,-default-");
2912 match def {
2913 CompFileDef::CompDef(CompDef::Commands(cmds)) => {
2914 assert!(cmds.contains(&"-value-".to_string()));
2915 assert!(cmds.contains(&"-array-value-".to_string()));
2916 assert!(cmds.contains(&"-value-,-default-,-default-".to_string()));
2917 }
2918 other => panic!("Expected Commands, got {:?}", other),
2919 }
2920 }
2921
2922 #[test]
2923 fn test_is_context_entry() {
2924 assert!(is_context_entry("-default-"));
2925 assert!(is_context_entry("-redirect-"));
2926 assert!(is_context_entry("-value-,DISPLAY,-default-"));
2927 assert!(is_context_entry("-redirect-,<,bunzip2=bunzip2"));
2928 assert!(is_context_entry("-redirect-,>,bzip2"));
2929 assert!(!is_context_entry("-p")); // option flag, not context
2930 assert!(!is_context_entry("-P")); // option flag
2931 assert!(!is_context_entry("git")); // regular command
2932 }
2933
2934 // =================================================================
2935 // compdef() tests — faithful to upstream `Completion/compinit`
2936 // sh:253-446. Lock the global state via reset_compdef_state to
2937 // isolate each case.
2938 // =================================================================
2939
2940 fn run(args: &[&str]) -> i32 {
2941 let owned: Vec<String> = args.iter().map(|s| s.to_string()).collect();
2942 compdef(&owned)
2943 }
2944
2945 #[test]
2946 fn compdef_empty_args_errors() {
2947 let _g = crate::test_util::global_state_lock();
2948 reset_compdef_state();
2949 assert_eq!(compdef(&[]), 1);
2950 }
2951
2952 #[test]
2953 fn compdef_normal_registration() {
2954 // sh:407-419 — `compdef _git git` writes `_comps[git]=_git`.
2955 let _g = crate::test_util::global_state_lock();
2956 reset_compdef_state();
2957 assert_eq!(run(&["_git", "git", "git-commit", "git-push"]), 0);
2958 let state = snapshot_compdef_state();
2959 assert_eq!(state.comps.get("git"), Some(&"_git".to_string()));
2960 assert_eq!(state.comps.get("git-commit"), Some(&"_git".to_string()));
2961 assert_eq!(state.comps.get("git-push"), Some(&"_git".to_string()));
2962 }
2963
2964 #[test]
2965 fn compdef_normal_with_service() {
2966 // sh:408-414 — `cmd=svc` records the service alongside.
2967 let _g = crate::test_util::global_state_lock();
2968 reset_compdef_state();
2969 assert_eq!(run(&["_git", "hub=git"]), 0);
2970 let state = snapshot_compdef_state();
2971 assert_eq!(state.comps.get("hub"), Some(&"_git".to_string()));
2972 assert_eq!(state.services.get("hub"), Some(&"git".to_string()));
2973 }
2974
2975 #[test]
2976 fn compdef_pattern_via_dash_p() {
2977 // sh:393-398 — `-p` writes into `_patcomps`.
2978 let _g = crate::test_util::global_state_lock();
2979 reset_compdef_state();
2980 assert_eq!(run(&["-p", "_test", "*-test"]), 0);
2981 let state = snapshot_compdef_state();
2982 assert_eq!(state.patcomps.get("*-test"), Some(&"_test".to_string()));
2983 }
2984
2985 #[test]
2986 fn compdef_postpattern_via_dash_p_caps() {
2987 // sh:400-405 — `-P` → `_postpatcomps`.
2988 let _g = crate::test_util::global_state_lock();
2989 reset_compdef_state();
2990 assert_eq!(run(&["-P", "_last", "_*"]), 0);
2991 let state = snapshot_compdef_state();
2992 assert_eq!(state.postpatcomps.get("_*"), Some(&"_last".to_string()));
2993 }
2994
2995 #[test]
2996 fn compdef_pattern_with_eq_rewrites_to_eq_form() {
2997 // sh:394-397 — `key=val` form is rewritten to `=val=func`.
2998 let _g = crate::test_util::global_state_lock();
2999 reset_compdef_state();
3000 assert_eq!(run(&["-p", "_test", "*=postfix"]), 0);
3001 let state = snapshot_compdef_state();
3002 assert_eq!(state.patcomps.get("*"), Some(&"=postfix=_test".to_string()));
3003 }
3004
3005 #[test]
3006 fn compdef_delete_removes_from_comps() {
3007 // sh:426-444 — `-d` deletes from the right hash.
3008 let _g = crate::test_util::global_state_lock();
3009 reset_compdef_state();
3010 run(&["_git", "git"]);
3011 assert!(snapshot_compdef_state().comps.contains_key("git"));
3012 assert_eq!(run(&["-d", "git"]), 0);
3013 assert!(!snapshot_compdef_state().comps.contains_key("git"));
3014 }
3015
3016 #[test]
3017 fn compdef_delete_pattern_removes_from_patcomps() {
3018 // sh:429-432 — `-d -p` deletes a pattern entry.
3019 let _g = crate::test_util::global_state_lock();
3020 reset_compdef_state();
3021 run(&["-p", "_test", "*-test"]);
3022 assert!(snapshot_compdef_state().patcomps.contains_key("*-test"));
3023 assert_eq!(run(&["-d", "-p", "*-test"]), 0);
3024 assert!(!snapshot_compdef_state().patcomps.contains_key("*-test"));
3025 }
3026
3027 #[test]
3028 fn compdef_no_clobber_skips_existing() {
3029 // sh:415 — `-n` keeps the existing binding.
3030 let _g = crate::test_util::global_state_lock();
3031 reset_compdef_state();
3032 run(&["_first", "git"]);
3033 run(&["-n", "_second", "git"]);
3034 assert_eq!(
3035 snapshot_compdef_state().comps.get("git"),
3036 Some(&"_first".to_string())
3037 );
3038 }
3039
3040 #[test]
3041 fn compdef_no_clobber_honours_a_registration_only_the_parameter_holds() {
3042 // sh:415 tests `[[ -z ${_comps[$1]} ]]` — the PARAMETER. Everything
3043 // `compinit -C` loaded from the dump lives only there, so checking
3044 // `CompdefState` alone let a later `compdef -n` overwrite it.
3045 let _g = crate::test_util::global_state_lock();
3046 reset_compdef_state();
3047 crate::ported::params::sethparam(
3048 "_comps",
3049 vec!["git".to_string(), "_git_from_dump".to_string()],
3050 );
3051 run(&["-n", "_second", "git"]);
3052 assert_eq!(
3053 crate::ported::subst::assoc_get("_comps")
3054 .and_then(|m| m.get("git").cloned())
3055 .as_deref(),
3056 Some("_git_from_dump")
3057 );
3058 }
3059
3060 #[test]
3061 fn compdef_keeps_registrations_it_did_not_make() {
3062 // The regression this whole merge-on-publish design exists for.
3063 // `compinit -C`'s cache-hit path fills `_comps` directly
3064 // (ext_builtins.rs, `set_assoc`) and never touches `CompdefState`,
3065 // so publishing the state wholesale replaced ~51k registrations
3066 // with the one key this process happened to register — after which
3067 // `_dispatch` resolved an empty completer for every command and
3068 // `man <TAB>` / `git <TAB>` / `kill <TAB>` all produced nothing.
3069 let _g = crate::test_util::global_state_lock();
3070 reset_compdef_state();
3071 crate::ported::params::sethparam(
3072 "_comps",
3073 vec![
3074 "man".to_string(),
3075 "_man".to_string(),
3076 "git".to_string(),
3077 "_git".to_string(),
3078 ],
3079 );
3080 assert_eq!(run(&["_zstyle", "zstyle"]), 0);
3081 let comps = crate::ported::subst::assoc_get("_comps").expect("_comps must still be a hash");
3082 assert_eq!(comps.get("man").map(String::as_str), Some("_man"));
3083 assert_eq!(comps.get("git").map(String::as_str), Some("_git"));
3084 assert_eq!(comps.get("zstyle").map(String::as_str), Some("_zstyle"));
3085 }
3086
3087 #[test]
3088 fn compdef_delete_removes_a_key_only_the_parameter_holds() {
3089 // The flip side: sh:442 `unset "_comps[$^@]"` has to reach an entry
3090 // that came from the dump, which a merge cannot express by omission.
3091 let _g = crate::test_util::global_state_lock();
3092 reset_compdef_state();
3093 crate::ported::params::sethparam(
3094 "_comps",
3095 vec![
3096 "man".to_string(),
3097 "_man".to_string(),
3098 "git".to_string(),
3099 "_git".to_string(),
3100 ],
3101 );
3102 assert_eq!(run(&["-d", "man"]), 0);
3103 let comps = crate::ported::subst::assoc_get("_comps").expect("_comps must still be a hash");
3104 assert_eq!(comps.get("man"), None);
3105 assert_eq!(comps.get("git").map(String::as_str), Some("_git"));
3106 }
3107
3108 #[test]
3109 fn compdef_batch_defers_publication_but_still_publishes() {
3110 // The batch must be a deferral, not a drop: a `cdreplay` whose
3111 // registrations never reached `_comps` would be the same outage
3112 // by another route.
3113 let _g = crate::test_util::global_state_lock();
3114 reset_compdef_state();
3115 compdef_batch(|| {
3116 run(&["_git", "git"]);
3117 assert!(
3118 crate::ported::subst::assoc_get("_comps")
3119 .map(|m| m.is_empty())
3120 .unwrap_or(true),
3121 "publication must be held until the batch ends"
3122 );
3123 run(&["_man", "man"]);
3124 });
3125 let comps = crate::ported::subst::assoc_get("_comps").expect("_comps must still be a hash");
3126 assert_eq!(comps.get("git").map(String::as_str), Some("_git"));
3127 assert_eq!(comps.get("man").map(String::as_str), Some("_man"));
3128 }
3129
3130 #[test]
3131 fn cache_is_valid_rejects_a_cache_another_build_wrote() {
3132 let cache = crate::compsys::cache::CompsysCache::memory().expect("in-memory cache");
3133 cache.set_comp("git", "_git").unwrap();
3134 assert!(stamp_cache_complete(&cache));
3135 assert!(cache_is_valid(&cache), "our own stamp must be accepted");
3136
3137 // Same rows, same count, stamped by a binary that is not this
3138 // one. Equality, so it is rejected whether that binary is older
3139 // or newer than the running one.
3140 cache
3141 .set_metadata(CACHE_BINARY_KEY, "1.2.3")
3142 .expect("restamp");
3143 assert!(
3144 !cache_is_valid(&cache),
3145 "a cache built by another zshrs must be rebuilt, not read"
3146 );
3147 }
3148
3149 #[test]
3150 fn cache_is_valid_rejects_a_cache_that_is_still_filling() {
3151 // A row count alone cannot distinguish "finished" from "another
3152 // shell is 200 rows into a 50k-row rebuild" — accepting the latter
3153 // is what published a `_comps` with a handful of entries.
3154 let cache = crate::compsys::cache::CompsysCache::memory().expect("in-memory cache");
3155 assert!(!cache_is_valid(&cache), "an empty cache is not valid");
3156 cache.set_comp("git", "_git").unwrap();
3157 assert!(
3158 !cache_is_valid(&cache),
3159 "a cache no build has stamped is not valid, however many rows it has"
3160 );
3161 assert!(stamp_cache_complete(&cache));
3162 assert!(cache_is_valid(&cache));
3163 cache.set_comp("man", "_man").unwrap();
3164 assert!(
3165 !cache_is_valid(&cache),
3166 "a row written after the stamp means the build was not the last writer"
3167 );
3168 }
3169
3170 #[test]
3171 fn compdef_inline_type_switch_dash_p() {
3172 // sh:385-390 — bare `-p` mid-args toggles to pattern mode.
3173 let _g = crate::test_util::global_state_lock();
3174 reset_compdef_state();
3175 run(&["_x", "cmd1", "-p", "pat*", "-N", "cmd2"]);
3176 let s = snapshot_compdef_state();
3177 assert_eq!(s.comps.get("cmd1"), Some(&"_x".to_string()));
3178 assert_eq!(s.patcomps.get("pat*"), Some(&"_x".to_string()));
3179 assert_eq!(s.comps.get("cmd2"), Some(&"_x".to_string()));
3180 }
3181
3182 #[test]
3183 fn compdef_combined_flags_an() {
3184 // sh:267 getopts allows `-an` combined.
3185 let _g = crate::test_util::global_state_lock();
3186 reset_compdef_state();
3187 // `-an` = autol + new
3188 assert_eq!(run(&["-an", "_git", "git"]), 0);
3189 let s = snapshot_compdef_state();
3190 assert_eq!(s.comps.get("git"), Some(&"_git".to_string()));
3191 // -a triggers compautos registration
3192 assert_eq!(s.compautos.get("_git"), Some(&"-rUz".to_string()));
3193 }
3194
3195 #[test]
3196 fn compdef_service_alias_mode_resolves_existing_func() {
3197 // sh:298-326 — first arg with `=` triggers service-alias.
3198 // Each entry resolves via `_services[(r)$svc]` reverse +
3199 // `_comps[$svc]`.
3200 let _g = crate::test_util::global_state_lock();
3201 reset_compdef_state();
3202 run(&["_git", "git"]); // first set up git→_git
3203 // Now `hub=git` should reuse _git
3204 assert_eq!(run(&["hub=git"]), 0);
3205 let s = snapshot_compdef_state();
3206 assert_eq!(s.comps.get("hub"), Some(&"_git".to_string()));
3207 assert_eq!(s.services.get("hub"), Some(&"git".to_string()));
3208 }
3209
3210 #[test]
3211 fn compdef_service_alias_unknown_returns_one() {
3212 // sh:316-318 unknown svc → error
3213 let _g = crate::test_util::global_state_lock();
3214 reset_compdef_state();
3215 assert_eq!(run(&["xyz=never-registered"]), 1);
3216 }
3217
3218 #[test]
3219 fn compdef_unknown_flag_errors() {
3220 let _g = crate::test_util::global_state_lock();
3221 reset_compdef_state();
3222 assert_eq!(run(&["-z", "_x", "cmd"]), 1);
3223 }
3224
3225 #[test]
3226 fn compdef_publishes_state_to_shell_arrays() {
3227 // `_comps` is an ASSOCIATIVE array in zsh (`typeset -gHA`), so the
3228 // shell-side view must be a hash where `${_comps[git]}` == `_git`.
3229 // (Previously published via setaparam as a flat array, which broke
3230 // `${_comps[cmd]}` key lookup and every completion — Bug #655.)
3231 let _g = crate::test_util::global_state_lock();
3232 reset_compdef_state();
3233 run(&["_git", "git"]);
3234 // Must be a proper association, not a flat array.
3235 let map = crate::ported::params::paramtab_hashed_storage()
3236 .lock()
3237 .unwrap()
3238 .get("_comps")
3239 .cloned()
3240 .expect("_comps must be a hashed (associative) param");
3241 assert_eq!(map.get("git").map(String::as_str), Some("_git"));
3242 }
3243
3244 /// compinit registers every scanned file with `compdef -na`, and `-n`
3245 /// keeps an EXISTING `_comps` entry (Completion/compinit sh:393). So
3246 /// when two completers claim the same command, the one in the earlier
3247 /// `$fpath` directory owns it.
3248 ///
3249 /// Regression: the scan inserted unconditionally, so the LAST writer
3250 /// won. On this host `_df` (`#compdef df gdf`, fpath[24]) lost to
3251 /// zsh-more-completions' `_dwarffortress` (`#compdef dwarffortress
3252 /// df`, fpath[42]) and `df -<TAB>` completed Dwarf Fortress options,
3253 /// i.e. nothing.
3254 #[test]
3255 fn scan_keeps_first_fpath_claim_on_a_command() {
3256 let _g = crate::test_util::global_state_lock();
3257 let base = std::env::temp_dir().join("zshrs_compinit_firstwins_test");
3258 let early = base.join("early");
3259 let late = base.join("late");
3260 let _ = fs::remove_dir_all(&base);
3261 fs::create_dir_all(&early).unwrap();
3262 fs::create_dir_all(&late).unwrap();
3263 // Same command claimed by two different files in two directories.
3264 fs::write(early.join("_zzcmd"), "#compdef zzcmd zzother\n").unwrap();
3265 fs::write(late.join("_zzgame"), "#compdef zzgame zzcmd\n").unwrap();
3266
3267 let result = compinit(&[early.clone(), late.clone()]);
3268 assert_eq!(
3269 result.comps.get("zzcmd").map(String::as_str),
3270 Some("_zzcmd"),
3271 "earlier fpath dir must keep the command"
3272 );
3273 // The later file still owns the commands nobody claimed first.
3274 assert_eq!(
3275 result.comps.get("zzgame").map(String::as_str),
3276 Some("_zzgame")
3277 );
3278
3279 // Reversing fpath order reverses the winner — order is what decides.
3280 let reversed = compinit(&[late.clone(), early.clone()]);
3281 assert_eq!(
3282 reversed.comps.get("zzcmd").map(String::as_str),
3283 Some("_zzgame")
3284 );
3285 let _ = fs::remove_dir_all(&base);
3286 }
3287
3288 /// Regression: `compinit` must leave an autoload stub in `shfunctab`
3289 /// for every completer it registers (sh:337 `autoload -rUz "$func"`,
3290 /// reached via the `compdef -na` at sh:541), because completers read
3291 /// `$functions` to discover their siblings — `_tmux` derives its
3292 /// sub-command list from `${(M)${(k)functions}:#_tmux-*}`
3293 /// (_tmux sh:1967). zshrs bulk-loaded `$_comps` without this step, so
3294 /// `tmux <TAB>` was missing the five `_tmux-*` helpers in `$fpath`.
3295 #[test]
3296 fn scan_registers_autoload_stubs_for_every_completer() {
3297 let _g = crate::test_util::global_state_lock();
3298 let dir = std::env::temp_dir().join("zshrs_compinit_stubs_test");
3299 let _ = fs::remove_dir_all(&dir);
3300 fs::create_dir_all(&dir).unwrap();
3301 fs::write(dir.join("_zzt"), "#compdef zzt\n").unwrap();
3302 fs::write(dir.join("_zzt-helper"), "#compdef zzt-helper\n").unwrap();
3303 fs::write(dir.join("_zzt_util"), "#autoload\n").unwrap();
3304 // A file with neither header contributes no function.
3305 fs::write(dir.join("_zzt_readme"), "just text\n").unwrap();
3306
3307 // A name already defined must survive untouched — `bin_functions`
3308 // leaves an existing function alone.
3309 for n in ["_zzt", "_zzt-helper", "_zzt_util", "_zzt_readme"] {
3310 if let Ok(mut t) = crate::ported::hashtable::shfunctab_lock().write() {
3311 t.remove(n);
3312 }
3313 }
3314 if let Ok(mut t) = crate::ported::hashtable::shfunctab_lock().write() {
3315 let mut defined = crate::ported::hashtable::shfunc_autoload("_zzt");
3316 defined.node.flags = 0;
3317 defined.body = Some("true".to_string());
3318 t.add(defined);
3319 }
3320
3321 let result = compinit(&[dir.clone()]);
3322 let names = autoload_stub_names(&result);
3323 assert!(names.contains(&"_zzt-helper"), "got {names:?}");
3324 assert!(names.contains(&"_zzt_util"), "got {names:?}");
3325 assert!(!names.contains(&"_zzt_readme"), "got {names:?}");
3326
3327 assert_eq!(
3328 register_autoload_stubs(&names),
3329 2,
3330 "the already-defined _zzt must not be re-stubbed"
3331 );
3332
3333 let tab = crate::ported::hashtable::shfunctab_lock();
3334 let tab = tab.read().unwrap();
3335 for n in ["_zzt-helper", "_zzt_util"] {
3336 let shf = tab.get(n).unwrap_or_else(|| panic!("{n} has no stub"));
3337 let flags = shf.node.flags as u32;
3338 assert!(flags & crate::ported::zsh_h::PM_UNDEFINED != 0, "{n}");
3339 assert!(flags & crate::ported::zsh_h::PM_UNALIASED != 0, "{n}");
3340 }
3341 assert_eq!(
3342 tab.get("_zzt").and_then(|f| f.body.clone()),
3343 Some("true".to_string()),
3344 "an already-defined function must keep its body"
3345 );
3346 assert!(tab.get("_zzt_readme").is_none());
3347 drop(tab);
3348 let _ = fs::remove_dir_all(&dir);
3349 }
3350
3351 /// Regression: `compinit -C -d FILE` takes the sh:515-518 branch, which
3352 /// sources the dump instead of scanning `$fpath` — so the dump's
3353 /// `autoload` lines, not a header scan, decide what ends up in
3354 /// `${(k)functions}`. compdump lists every defined `_*` function that
3355 /// has a file in `$fpath` (compdump:113), which is why the real dump on
3356 /// a zpwr host names 12 headerless helpers (`_command_names`,
3357 /// `__zpwr_aliases`, …) that no `#compdef`/`#autoload` scan can find.
3358 /// This asserts the parse of both line shapes compdump emits: the one
3359 /// backslash-continued `autoload -Uz a b c` list (compdump:118-129) and
3360 /// the per-`$_compautos` `autoload -Uz <opts> <name>` lines
3361 /// (compdump:135-138).
3362 #[test]
3363 fn dump_autoload_names_reads_both_compdump_line_shapes() {
3364 let dir = std::env::temp_dir().join("zshrs_compinit_dumpnames_test");
3365 let _ = fs::remove_dir_all(&dir);
3366 fs::create_dir_all(&dir).unwrap();
3367 let dump = dir.join("zcompdump");
3368 fs::write(
3369 &dump,
3370 concat!(
3371 "#files: 3\tversion: 5.9.2\n",
3372 "\n",
3373 "_comps=(\n",
3374 // A _comps KEY may literally be `autoload`; the quoting must
3375 // keep it out of the name list.
3376 "'autoload' '_autoload'\n",
3377 "'zzt' '_zzt'\n",
3378 ")\n",
3379 "\n",
3380 "zle -C _complete_help complete-word _complete_help\n",
3381 "bindkey '^Xh' _complete_help\n",
3382 "\n",
3383 "autoload -Uz _zzt _zzt_two \\\n",
3384 " __zzt_headerless _zzt_gone\n",
3385 "autoload -Uz +X _call_program\n",
3386 "typeset -gUa _comp_assocs\n",
3387 ),
3388 )
3389 .unwrap();
3390
3391 let names = dump_autoload_names(&dump);
3392 assert_eq!(
3393 names,
3394 vec![
3395 "_zzt",
3396 "_zzt_two",
3397 "__zzt_headerless",
3398 "_zzt_gone",
3399 "_call_program",
3400 ],
3401 "continuation lines, `+X`/`-Uz` option words and the quoted \
3402 `_comps` key must all be handled"
3403 );
3404 let _ = fs::remove_dir_all(&dir);
3405 }
3406
3407 /// Regression: on the sh:493-496 `-C` branch the dump is sourced and
3408 /// sh:501's `[[ -z "$_i_done" ]]` skips the `$fpath` scan, so the dump
3409 /// alone defines all five association tables. zshrs read them from its
3410 /// SQLite cache instead, and a partially-built cache (1849 `_comps`
3411 /// keys against the real dump's 51745 on a zpwr host) silently dropped
3412 /// `$_comps[zpwr]`, `$_comps[cargo]`, `$_comps[brew]`, … — every one of
3413 /// those commands then fell through `_dispatch` to `-default-` and
3414 /// completed FILES where zsh runs the registered completer.
3415 ///
3416 /// The value shapes asserted here are the ones compdump's `${(qq)}`
3417 /// actually emits (compdump:38-70): a plain `'k' 'v'` pair, a key that
3418 /// starts with a literal quote (`''\''brew'` → `'brew`), and a key with
3419 /// an embedded one (`'services'\'''` → `services'`).
3420 #[test]
3421 fn dump_assoc_tables_reads_all_five_compdump_tables() {
3422 let dir = std::env::temp_dir().join("zshrs_compinit_dumptables_test");
3423 let _ = fs::remove_dir_all(&dir);
3424 fs::create_dir_all(&dir).unwrap();
3425 let dump = dir.join("zcompdump");
3426 fs::write(
3427 &dump,
3428 concat!(
3429 "#files: 3\tversion: 5.9.2\n",
3430 "\n",
3431 "_comps=(\n",
3432 "'zpwr' '_zpwr'\n",
3433 "''\\''brew' '_brew_services'\n",
3434 "'services'\\''' '_brew_services'\n",
3435 ")\n",
3436 "\n",
3437 "_services=(\n",
3438 "'ftp' 'ftp'\n",
3439 ")\n",
3440 "\n",
3441 "_patcomps=(\n",
3442 "'*/(init|rc[0-9S]#).d/*' '_init_d'\n",
3443 ")\n",
3444 "\n",
3445 "_postpatcomps=(\n",
3446 "'_*' '_compadd'\n",
3447 "'gcc-*' '_gcc'\n",
3448 ")\n",
3449 "\n",
3450 "_compautos=(\n",
3451 "'_call_program' '+X'\n",
3452 ")\n",
3453 "\n",
3454 // Everything after the tables must be ignored, including a
3455 // `)` that does not close one.
3456 "zle -C _complete_help complete-word _complete_help\n",
3457 "autoload -Uz _zzt\n",
3458 "typeset -gUa _comp_assocs\n",
3459 "_comp_assocs=( '' )\n",
3460 ),
3461 )
3462 .unwrap();
3463
3464 let t = dump_assoc_tables(&dump).expect("dump is readable");
3465 assert_eq!(t.comps.get("zpwr").map(String::as_str), Some("_zpwr"));
3466 assert_eq!(
3467 t.comps.get("'brew").map(String::as_str),
3468 Some("_brew_services"),
3469 "`''\\''brew'` is three concatenated (qq) segments = `'brew`"
3470 );
3471 assert_eq!(
3472 t.comps.get("services'").map(String::as_str),
3473 Some("_brew_services")
3474 );
3475 assert_eq!(t.comps.len(), 3, "no stray keys from the trailing lines");
3476 assert_eq!(t.services.get("ftp").map(String::as_str), Some("ftp"));
3477 assert_eq!(
3478 t.patcomps.get("*/(init|rc[0-9S]#).d/*").map(String::as_str),
3479 Some("_init_d")
3480 );
3481 // Order is load-bearing: `_postpatcomps` is tried in insertion order,
3482 // and compdump writes it in `${(ok)}` order (compdump:61-66).
3483 assert_eq!(
3484 t.postpatcomps
3485 .keys()
3486 .map(String::as_str)
3487 .collect::<Vec<_>>(),
3488 vec!["_*", "gcc-*"]
3489 );
3490 assert_eq!(
3491 t.compautos.get("_call_program").map(String::as_str),
3492 Some("+X")
3493 );
3494 let _ = fs::remove_dir_all(&dir);
3495 }
3496}