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