@optique/core 1.0.15-dev.2849 → 1.0.15-dev.2873
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/completion.cjs
CHANGED
|
@@ -120,7 +120,7 @@ function _${programName} () {
|
|
|
120
120
|
esac
|
|
121
121
|
fi
|
|
122
122
|
done
|
|
123
|
-
local __tilde_pattern='^(~[a-zA-Z0-9_.+-]*)(/.*)
|
|
123
|
+
local __tilde_pattern='^(~[a-zA-Z0-9_.+-]*)(/.*)?$'
|
|
124
124
|
local __tilde_glob_pattern='^(~[a-zA-Z0-9_.+-]*)(/.*)?$'
|
|
125
125
|
local prev=("\${COMP_WORDS[@]:1:COMP_CWORD-1}")
|
|
126
126
|
# Bash splits unquoted '=' at its default word breaks. Reassemble only
|
|
@@ -353,8 +353,9 @@ function _${programName} () {
|
|
|
353
353
|
|
|
354
354
|
# path() echoes the raw shell word as its pattern. Once dequoted,
|
|
355
355
|
# use the literal current-word prefix instead of the escaped pattern.
|
|
356
|
-
if [[
|
|
357
|
-
|
|
356
|
+
if [[ "$pattern" == "$__raw_file_current" && "$__file_current" != "$__raw_file_current" ]] ||
|
|
357
|
+
{ [[ -n "$__reply_prefix" ]] && [ "$pattern" = "$__file_current" ] &&
|
|
358
|
+
{ [[ "$pattern" != *[\*\?]* ]] || [ "$__raw_file_current" != "$__file_current" ]; }; }; then
|
|
358
359
|
pattern=""
|
|
359
360
|
fi
|
|
360
361
|
|
|
@@ -388,7 +389,8 @@ function _${programName} () {
|
|
|
388
389
|
# However, if the user has already typed beyond the pattern (e.g.,
|
|
389
390
|
# pattern="src/" and current="src/ma"), preserve the typed suffix
|
|
390
391
|
# for incremental filtering.
|
|
391
|
-
local __from_pattern=0
|
|
392
|
+
local __from_pattern=0 __filter_pattern=0 __pattern_prefix=""
|
|
393
|
+
local __pattern_tilde_prefix="" __pattern_tilde_expanded=""
|
|
392
394
|
if [[ -n "$pattern" ]]; then
|
|
393
395
|
# Normalize leading ./ so that ./src/ma matches pattern src/
|
|
394
396
|
local __norm_current="\${__file_current#./}"
|
|
@@ -400,10 +402,27 @@ function _${programName} () {
|
|
|
400
402
|
__compare_pattern="\${__compare_pattern%%[\*\?]*}"
|
|
401
403
|
[[ "$__compare_pattern" == */* ]] && __compare_pattern="\${__compare_pattern%/*}/" || __compare_pattern=""
|
|
402
404
|
fi
|
|
405
|
+
local __extends_pattern=0
|
|
403
406
|
if [[ ( -n "$__compare_pattern" || -n "$__norm_current" ) && \${#__norm_current} -ge \${#__compare_pattern} && "\${__norm_current:0:\${#__compare_pattern}}" == "$__compare_pattern" && "$__file_current" != "$pattern" ]]; then
|
|
407
|
+
__extends_pattern=1
|
|
408
|
+
fi
|
|
409
|
+
if (( __extends_pattern )) && [[ "$__norm_pattern" != *[\*\?]* ]]; then
|
|
404
410
|
# User has typed beyond or an equivalent form of the pattern
|
|
405
411
|
true
|
|
406
412
|
else
|
|
413
|
+
# Wildcards keep their file filter while the typed prefix narrows
|
|
414
|
+
# the resulting paths. Save the expanded prefix before using pattern.
|
|
415
|
+
if [[ "$__norm_pattern" == *[\*\?]* ]]; then
|
|
416
|
+
__filter_pattern=1
|
|
417
|
+
__pattern_prefix="$__glob_current"
|
|
418
|
+
# An unquoted value equal to its wildcard is a complete search
|
|
419
|
+
# pattern, not a literal filename prefix to filter against.
|
|
420
|
+
if [ "$__file_current" = "$pattern" ] && [ "$__raw_file_current" = "$__file_current" ]; then
|
|
421
|
+
__pattern_prefix=""
|
|
422
|
+
fi
|
|
423
|
+
__pattern_tilde_prefix="$__tilde_prefix"
|
|
424
|
+
__pattern_tilde_expanded="$__tilde_expanded"
|
|
425
|
+
fi
|
|
407
426
|
__from_pattern=1
|
|
408
427
|
# Reset tilde state from the current-word expansion so that a
|
|
409
428
|
# non-tilde pattern is not rewritten through stale tilde state
|
|
@@ -432,36 +451,93 @@ function _${programName} () {
|
|
|
432
451
|
# This runs after tilde expansion so that paths like ~/.config/ are
|
|
433
452
|
# checked against the expanded path, not the literal ~ string.
|
|
434
453
|
local __inside_hidden_path=0
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
454
|
+
local __hidden_directory_pattern='/[.]([^./][^/]*|[.][^/]+)/'
|
|
455
|
+
if [[ "/\${__glob_current%/}/" =~ $__hidden_directory_pattern ||
|
|
456
|
+
"/\${__pattern_prefix%/}/" =~ $__hidden_directory_pattern ]]; then
|
|
457
|
+
__inside_hidden_path=1
|
|
458
|
+
fi
|
|
438
459
|
# Also check if the current prefix explicitly targets hidden entries
|
|
439
460
|
# (e.g., user typed "." or ".e" to complete .env)
|
|
440
461
|
local __prefix_targets_hidden=0
|
|
441
462
|
local __prefix_base="\${__glob_current##*/}"
|
|
463
|
+
if [[ -n "$__pattern_prefix" ]]; then __prefix_base="\${__pattern_prefix##*/}"; fi
|
|
442
464
|
[[ "$__prefix_base" == .* ]] && __prefix_targets_hidden=1
|
|
443
465
|
if [[ "$hidden" == "1" || "$__inside_hidden_path" == "1" || "$__prefix_targets_hidden" == "1" ]]; then shopt -s dotglob; fi
|
|
444
466
|
|
|
445
467
|
# Pre-expand file candidates. When the glob base came from the
|
|
446
468
|
# program's pattern and contains wildcard characters (* or ?),
|
|
447
|
-
#
|
|
469
|
+
# expand it as data with shell globbing. Empty IFS avoids splitting paths;
|
|
470
|
+
# NUL records preserve spaces and newlines without requiring mapfile.
|
|
471
|
+
# Shell expansion respects dotglob on older Bash versions as well.
|
|
448
472
|
# Otherwise append * to treat it as a prefix.
|
|
449
|
-
#
|
|
450
|
-
#
|
|
473
|
+
# Only * and ? are wildcard syntax; escape brackets, backslashes, and
|
|
474
|
+
# extended-glob operators so other pattern characters stay literal.
|
|
451
475
|
local -a __candidates=()
|
|
452
476
|
if [[ "$__from_pattern" == "1" && "$__glob_current" == *[\*\?]* ]]; then
|
|
453
|
-
|
|
477
|
+
local __pathname_glob="" __glob_char __glob_index
|
|
478
|
+
for ((__glob_index=0; __glob_index<\${#__glob_current}; __glob_index++)); do
|
|
479
|
+
__glob_char="\${__glob_current:__glob_index:1}"
|
|
480
|
+
case "$__glob_char" in
|
|
481
|
+
'['|']'|'\\'|'('|')'|'|'|'+'|'@'|'!')
|
|
482
|
+
__pathname_glob+="\\\\$__glob_char" ;;
|
|
483
|
+
*) __pathname_glob+="$__glob_char" ;;
|
|
484
|
+
esac
|
|
485
|
+
done
|
|
486
|
+
[[ "$type" == "directory" ]] && __pathname_glob="\${__pathname_glob%/}"
|
|
487
|
+
local __glob_dir="\${__pathname_glob%/*}"
|
|
488
|
+
[[ "$__glob_dir" == "$__pathname_glob" ]] && __glob_dir="."
|
|
489
|
+
[[ -z "$__glob_dir" ]] && __glob_dir="/"
|
|
490
|
+
local __navigation_dir="" __basename_glob="\${__pathname_glob##*/}"
|
|
491
|
+
# Once an offered directory is entered, keep the basename filter but
|
|
492
|
+
# enumerate there. Only descendants of matching pattern bases qualify.
|
|
493
|
+
if [[ "$__pattern_prefix" == */* ]]; then
|
|
494
|
+
local __typed_dir="\${__pattern_prefix%/*}" __base_dir __norm_base __norm_dir
|
|
495
|
+
# Resolve directory ancestry in subshells, preserving the typed spelling
|
|
496
|
+
# for insertion. The trailing slash also preserves newline-ending names.
|
|
497
|
+
if __norm_dir=$(CDPATH= builtin cd -P -- "$__typed_dir" >/dev/null 2>&1 && printf '%s/' "\${PWD%/}"); then
|
|
498
|
+
while IFS= read -r -d '' __base_dir; do
|
|
499
|
+
[[ -d "$__base_dir" ]] || continue
|
|
500
|
+
__norm_base=$(CDPATH= builtin cd -P -- "$__base_dir" >/dev/null 2>&1 && printf '%s/' "\${PWD%/}") || continue
|
|
501
|
+
if [ "\${__norm_dir:0:\${#__norm_base}}" = "$__norm_base" ]; then
|
|
502
|
+
__navigation_dir="$__typed_dir"
|
|
503
|
+
break
|
|
504
|
+
fi
|
|
505
|
+
done < <(IFS=; printf '%s\\0' $__glob_dir)
|
|
506
|
+
fi
|
|
507
|
+
fi
|
|
508
|
+
local __candidate
|
|
509
|
+
while IFS= read -r -d '' __candidate; do
|
|
510
|
+
if [[ -e "$__candidate" || -L "$__candidate" ]]; then
|
|
511
|
+
[[ -d "$__candidate" ]] && __candidate="\${__candidate%/}"
|
|
512
|
+
__candidates+=("$__candidate")
|
|
513
|
+
fi
|
|
514
|
+
done < <(
|
|
515
|
+
IFS=
|
|
516
|
+
if [[ -n "$__navigation_dir" ]]; then
|
|
517
|
+
printf '%s\\0' "$__navigation_dir"/$__basename_glob
|
|
518
|
+
else
|
|
519
|
+
printf '%s\\0' $__pathname_glob
|
|
520
|
+
fi
|
|
521
|
+
)
|
|
454
522
|
# For file/any modes, also include directories from the base
|
|
455
523
|
# directory for navigation even when the glob itself does not
|
|
456
524
|
# match directory names. Skip this for directory mode so that
|
|
457
525
|
# the pattern's basename filter is respected.
|
|
458
526
|
if [[ "$type" != "directory" ]]; then
|
|
459
|
-
local __glob_dir="\${__glob_current%/*}"
|
|
460
|
-
[[ "$__glob_dir" == "$__glob_current" ]] && __glob_dir="."
|
|
461
527
|
local __d
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
528
|
+
local __directory_pattern="\${__glob_dir%/}/*/"
|
|
529
|
+
while IFS= read -r -d '' __d; do
|
|
530
|
+
[[ -d "$__d" ]] || continue
|
|
531
|
+
__d="\${__d%/}"
|
|
532
|
+
__candidates+=("$__d")
|
|
533
|
+
done < <(
|
|
534
|
+
IFS=
|
|
535
|
+
if [[ -n "$__navigation_dir" ]]; then
|
|
536
|
+
printf '%s\\0' "$__navigation_dir"/*/
|
|
537
|
+
else
|
|
538
|
+
printf '%s\\0' $__directory_pattern
|
|
539
|
+
fi
|
|
540
|
+
)
|
|
465
541
|
fi
|
|
466
542
|
else
|
|
467
543
|
__candidates=("$__glob_current"*)
|
|
@@ -472,6 +548,26 @@ function _${programName} () {
|
|
|
472
548
|
fi
|
|
473
549
|
fi
|
|
474
550
|
|
|
551
|
+
if (( __filter_pattern )); then
|
|
552
|
+
local __nocasematch_was_set=0
|
|
553
|
+
shopt -q nocasematch && __nocasematch_was_set=1
|
|
554
|
+
if shopt -q nocaseglob; then shopt -s nocasematch; else shopt -u nocasematch; fi
|
|
555
|
+
local -a __filtered_candidates=()
|
|
556
|
+
for __candidate in "\${__candidates[@]}"; do
|
|
557
|
+
if [[ "$__candidate" == "$__pattern_prefix"* ||
|
|
558
|
+
"\${__candidate#./}" == "\${__pattern_prefix#./}"* ]]; then
|
|
559
|
+
if [[ "$__pattern_prefix" == ./* && "$__candidate" != ./* ]]; then
|
|
560
|
+
__candidate="./$__candidate"
|
|
561
|
+
elif [[ "$__pattern_prefix" != ./* && "$__pattern_prefix" != . ]]; then
|
|
562
|
+
__candidate="\${__candidate#./}"
|
|
563
|
+
fi
|
|
564
|
+
__filtered_candidates+=("$__candidate")
|
|
565
|
+
fi
|
|
566
|
+
done
|
|
567
|
+
if (( __nocasematch_was_set )); then shopt -s nocasematch; else shopt -u nocasematch; fi
|
|
568
|
+
__candidates=("\${__filtered_candidates[@]}")
|
|
569
|
+
fi
|
|
570
|
+
|
|
475
571
|
# Generate file completions based on type
|
|
476
572
|
case "$type" in
|
|
477
573
|
file)
|
|
@@ -533,6 +629,10 @@ function _${programName} () {
|
|
|
533
629
|
esac
|
|
534
630
|
|
|
535
631
|
# Restore tilde prefix in completion results
|
|
632
|
+
if (( __filter_pattern )); then
|
|
633
|
+
__tilde_prefix="$__pattern_tilde_prefix"
|
|
634
|
+
__tilde_expanded="$__pattern_tilde_expanded"
|
|
635
|
+
fi
|
|
536
636
|
if [[ -n "$__tilde_prefix" &&
|
|
537
637
|
( -z "$__quote" || $__file_quote_offset -gt 0 ) ]]; then
|
|
538
638
|
local __i
|
|
@@ -632,6 +732,31 @@ function _${programName} () {
|
|
|
632
732
|
COMPREPLY+=("\${__word:\${#__remove}}")
|
|
633
733
|
fi
|
|
634
734
|
done
|
|
735
|
+
if (( \${#COMPREPLY[@]} > 1 )) && command -v sort >/dev/null 2>&1; then
|
|
736
|
+
# Sort shell-escaped keys, not filenames: each key occupies one line even
|
|
737
|
+
# when a candidate contains tabs or newlines. Keep the first occurrence's
|
|
738
|
+
# index, then restore the original order without evaluating escaped data.
|
|
739
|
+
# This works without associative arrays or NUL-aware sort on Bash 3.2.
|
|
740
|
+
local -a __unique_replies=()
|
|
741
|
+
local __index __key __last_key=""
|
|
742
|
+
while IFS= read -r __index; do
|
|
743
|
+
__unique_replies+=("\${COMPREPLY[__index]}")
|
|
744
|
+
done < <(
|
|
745
|
+
for ((__index=0; __index<\${#COMPREPLY[@]}; __index++)); do
|
|
746
|
+
printf '%q\\t%d\\n' "\${COMPREPLY[__index]}" "$__index"
|
|
747
|
+
done |
|
|
748
|
+
LC_ALL=C sort -t $'\\t' -k1,1 -k2,2n |
|
|
749
|
+
{
|
|
750
|
+
while IFS=$'\\t' read -r __key __index; do
|
|
751
|
+
if [ "$__key" != "$__last_key" ]; then
|
|
752
|
+
printf '%s\\n' "$__index"
|
|
753
|
+
__last_key="$__key"
|
|
754
|
+
fi
|
|
755
|
+
done
|
|
756
|
+
} | LC_ALL=C sort -n
|
|
757
|
+
)
|
|
758
|
+
COMPREPLY=("\${__unique_replies[@]}")
|
|
759
|
+
fi
|
|
635
760
|
if [[ $__file_mode == 1 && $(type -t compopt) == builtin ]]; then
|
|
636
761
|
compopt -o filenames 2>/dev/null || true
|
|
637
762
|
fi
|
|
@@ -657,7 +782,7 @@ fi
|
|
|
657
782
|
}
|
|
658
783
|
for (const [index, { prefix, file }] of joined.files.entries()) {
|
|
659
784
|
if (index > 0) yield "\n";
|
|
660
|
-
yield `__JOINED_FILE__:${encodePattern(prefix)}:${file.type}:${encodeExtensions(file.extensions)}
|
|
785
|
+
yield `__JOINED_FILE__:${encodePattern(prefix)}:${file.type}:${encodeExtensions(file.extensions)}:${encodePattern(sanitizeForTransport(file.pattern ?? ""))}:${file.includeHidden ? "1" : "0"}`;
|
|
661
786
|
}
|
|
662
787
|
} else if (suggestion.kind === "literal") yield encodeBashLiteral(suggestion.text);
|
|
663
788
|
else {
|
|
@@ -679,9 +804,14 @@ const zsh = {
|
|
|
679
804
|
generateScript(programName, args = []) {
|
|
680
805
|
validateProgramName(programName);
|
|
681
806
|
const escapedArgs = args.map((arg) => `'${arg.replace(/'/g, "'\\''")}'`).join(" ");
|
|
807
|
+
const functionName = /[.-]/.test(programName) ? `_optique:${programName}` : `_${programName}`;
|
|
682
808
|
return `#compdef ${programName}
|
|
683
|
-
function
|
|
809
|
+
function ${functionName} () {
|
|
684
810
|
local current="\$words[CURRENT]"
|
|
811
|
+
# Let zsh match the suffix when completing in the middle of a word.
|
|
812
|
+
if [[ -o complete_in_word ]]; then
|
|
813
|
+
current="\$PREFIX"
|
|
814
|
+
fi
|
|
685
815
|
local -a prev
|
|
686
816
|
# Extract previous arguments, skipping empty ones
|
|
687
817
|
prev=()
|
|
@@ -769,17 +899,60 @@ function _${programName.replace(/[^a-zA-Z0-9]/g, "_")} () {
|
|
|
769
899
|
# Use zsh's native file completion
|
|
770
900
|
case "\$type" in
|
|
771
901
|
file)
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
902
|
+
# Use the same regular-file filter with or without extensions,
|
|
903
|
+
# keeping symlinked files and directory navigation available.
|
|
904
|
+
local file_pattern="\${ext_pattern:-*}(#q-.)"
|
|
905
|
+
local file_subtree_pattern="**/*(#q/N)"
|
|
906
|
+
local -a expl=() file_subtree=() file_recursive_patterns=()
|
|
907
|
+
local -a file_tags=(files directories)
|
|
908
|
+
local file_recursive_pattern file_directory file_prefix file_tag
|
|
909
|
+
if [[ -z "\$ext_pattern" ]] &&
|
|
910
|
+
zstyle -t ":completion:\${curcontext}:" list-dirs-first; then
|
|
911
|
+
file_tags=(directories files)
|
|
782
912
|
fi
|
|
913
|
+
_tags "\${file_tags[@]}"
|
|
914
|
+
while _tags; do
|
|
915
|
+
local file_matched=0
|
|
916
|
+
for file_tag in "\${file_tags[@]}"; do
|
|
917
|
+
if [[ "\$file_tag" == directories ]]; then
|
|
918
|
+
while _next_label directories expl directory; do
|
|
919
|
+
_path_files -/ "\${expl[@]}" && file_matched=1
|
|
920
|
+
done
|
|
921
|
+
else
|
|
922
|
+
while _next_label files expl file; do
|
|
923
|
+
local -a file_ignore=() file_ignore_args=()
|
|
924
|
+
# Explicit globs bypass automatic fignore. Apply it only
|
|
925
|
+
# when the tag has not supplied its own ignore patterns.
|
|
926
|
+
if [[ -z "\$ext_pattern" && -z "\$_comp_no_ignore" && -n "\$FIGNORE" ]] && (( \${#_comp_ignore[@]} == 0 )); then
|
|
927
|
+
file_ignore=("?*\${^fignore[@]}")
|
|
928
|
+
file_ignore_args=(-F file_ignore)
|
|
929
|
+
fi
|
|
930
|
+
if _path_files -g "\${file_pattern}" "\${expl[@]}" "\${file_ignore_args[@]}"; then
|
|
931
|
+
file_matched=1
|
|
932
|
+
elif [[ -z "\$ext_pattern" && "\$PREFIX\$SUFFIX" != */* ]] &&
|
|
933
|
+
zstyle -a ":completion:\${curcontext}:files" recursive-files file_recursive_patterns; then
|
|
934
|
+
# Match _files' recursive fallback, retaining the same
|
|
935
|
+
# file filter and tag-specific ignore options.
|
|
936
|
+
for file_recursive_pattern in "\${file_recursive_patterns[@]}"; do
|
|
937
|
+
if [[ "\$PWD/" = \${~file_recursive_pattern} ]]; then
|
|
938
|
+
if (( \${#file_subtree[@]} == 0 )); then
|
|
939
|
+
file_subtree=(\${~file_subtree_pattern})
|
|
940
|
+
fi
|
|
941
|
+
for file_directory in "\${file_subtree[@]}"; do
|
|
942
|
+
file_prefix="\$PREFIX"
|
|
943
|
+
PREFIX="\$file_directory/\$PREFIX"
|
|
944
|
+
_path_files -g "\${file_pattern}" "\${expl[@]}" "\${file_ignore_args[@]}" && file_matched=1
|
|
945
|
+
PREFIX="\$file_prefix"
|
|
946
|
+
done
|
|
947
|
+
break
|
|
948
|
+
fi
|
|
949
|
+
done
|
|
950
|
+
fi
|
|
951
|
+
done
|
|
952
|
+
fi
|
|
953
|
+
done
|
|
954
|
+
(( file_matched )) && break
|
|
955
|
+
done
|
|
783
956
|
;;
|
|
784
957
|
directory)
|
|
785
958
|
_directories
|
|
@@ -829,7 +1002,13 @@ function _${programName.replace(/[^a-zA-Z0-9]/g, "_")} () {
|
|
|
829
1002
|
}
|
|
830
1003
|
|
|
831
1004
|
if (( $+functions[compdef] )); then
|
|
832
|
-
compdef
|
|
1005
|
+
compdef ${functionName} ${programName}
|
|
1006
|
+
fi
|
|
1007
|
+
|
|
1008
|
+
# An autoloaded file runs as a function body. Dispatch its initial request;
|
|
1009
|
+
# sourcing the file only defines and registers the completion helper.
|
|
1010
|
+
if [[ \$ZSH_EVAL_CONTEXT == *:loadautofunc || \$ZSH_EVAL_CONTEXT == *:shfunc ]]; then
|
|
1011
|
+
${functionName} "\$@"
|
|
833
1012
|
fi
|
|
834
1013
|
`;
|
|
835
1014
|
},
|
|
@@ -855,7 +1034,7 @@ const fish = {
|
|
|
855
1034
|
generateScript(programName, args = []) {
|
|
856
1035
|
validateProgramName(programName);
|
|
857
1036
|
const escapedArgs = args.map((arg) => `'${arg.replace(/'/g, "\\'")}'`).join(" ");
|
|
858
|
-
const functionName = `__${programName
|
|
1037
|
+
const functionName = `__${programName}_complete`;
|
|
859
1038
|
return `
|
|
860
1039
|
function ${functionName}
|
|
861
1040
|
set -l tokens (commandline -poc)
|
|
@@ -1544,8 +1723,9 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
|
|
|
1544
1723
|
let all_items = (do $list_entries $ls_pattern)
|
|
1545
1724
|
let dirs = $all_items | where type == dir
|
|
1546
1725
|
let files = $all_items | where type == file | where {|f|
|
|
1547
|
-
|
|
1548
|
-
|
|
1726
|
+
$ext_list | any {|ext|
|
|
1727
|
+
$f.name | path basename | str ends-with ('.' + $ext)
|
|
1728
|
+
}
|
|
1549
1729
|
}
|
|
1550
1730
|
$dirs | append $files
|
|
1551
1731
|
}
|
|
@@ -1561,8 +1741,9 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
|
|
|
1561
1741
|
let all_items = (do $list_entries $ls_pattern)
|
|
1562
1742
|
let dirs = $all_items | where type == dir
|
|
1563
1743
|
let files = $all_items | where type == file | where {|f|
|
|
1564
|
-
|
|
1565
|
-
|
|
1744
|
+
$ext_list | any {|ext|
|
|
1745
|
+
$f.name | path basename | str ends-with ('.' + $ext)
|
|
1746
|
+
}
|
|
1566
1747
|
}
|
|
1567
1748
|
$dirs | append $files
|
|
1568
1749
|
}
|
|
@@ -1730,6 +1911,15 @@ const pwsh = {
|
|
|
1730
1911
|
Register-ArgumentCompleter -Native -CommandName ${programName} -ScriptBlock {
|
|
1731
1912
|
param(\$wordToComplete, \$commandAst, \$cursorPosition)
|
|
1732
1913
|
|
|
1914
|
+
# CompletionText is shell syntax; display labels remain unquoted.
|
|
1915
|
+
\$quoteArgument = {
|
|
1916
|
+
param([string]\$value)
|
|
1917
|
+
if (\$value -cmatch '\\A[a-zA-Z0-9_./:\\\\-]+\\z' -and \$value -cnotmatch '\\A-(?!-).*[.:]' -and \$value -cnotmatch '\\A-?(?:[0-9]|\\.[0-9])') {
|
|
1918
|
+
return \$value
|
|
1919
|
+
}
|
|
1920
|
+
return "'" + [System.Management.Automation.Language.CodeGeneration]::EscapeSingleQuotedStringContent(\$value) + "'"
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1733
1923
|
# Extract arguments from AST (handles quoted strings properly)
|
|
1734
1924
|
\$arguments = @()
|
|
1735
1925
|
\$commandElements = \$commandAst.CommandElements
|
|
@@ -1743,8 +1933,11 @@ Register-ArgumentCompleter -Native -CommandName ${programName} -ScriptBlock {
|
|
|
1743
1933
|
if (\$element.Extent.EndOffset -ge \$cursorPosition) {
|
|
1744
1934
|
# wordToComplete has synthesized delimiters and decoded escapes.
|
|
1745
1935
|
# Only the AST retains the literal value and original quote type.
|
|
1936
|
+
# PSReadLine leaves the cursor before a directory's closing quote.
|
|
1746
1937
|
if (\$element -is [System.Management.Automation.Language.StringConstantExpressionAst] -and
|
|
1747
|
-
\$element.Extent.EndOffset -eq \$cursorPosition
|
|
1938
|
+
(\$element.Extent.EndOffset -eq \$cursorPosition -or
|
|
1939
|
+
(\$element.Extent.EndOffset -eq (\$cursorPosition + 1) -and
|
|
1940
|
+
\$element.StringConstantType -in @('SingleQuoted', 'DoubleQuoted')))) {
|
|
1748
1941
|
if (\$element.StringConstantType -in @('SingleQuoted', 'DoubleQuoted')) {
|
|
1749
1942
|
\$valuePrefix = \$element.Value
|
|
1750
1943
|
\$quoteType = \$element.StringConstantType
|
|
@@ -1777,7 +1970,7 @@ Register-ArgumentCompleter -Native -CommandName ${programName} -ScriptBlock {
|
|
|
1777
1970
|
} elseif (\$quoteType -eq 'DoubleQuoted') {
|
|
1778
1971
|
'"' + (\$text -replace '[\\x60\$"\\u201c-\\u201e]', '\`\$0') + '"'
|
|
1779
1972
|
} else {
|
|
1780
|
-
\$text
|
|
1973
|
+
& \$quoteArgument \$text
|
|
1781
1974
|
}
|
|
1782
1975
|
}
|
|
1783
1976
|
|
|
@@ -1882,7 +2075,10 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
|
|
|
1882
2075
|
\$extList = (\$extensions -split ',') -replace '^\\.',''
|
|
1883
2076
|
\$items = Get-ChildItem @forceParam -Path \$globPath -ErrorAction SilentlyContinue |
|
|
1884
2077
|
Where-Object {
|
|
1885
|
-
\$
|
|
2078
|
+
\$fileName = \$_.Name
|
|
2079
|
+
\$_.PSIsContainer -or (@(\$extList | Where-Object {
|
|
2080
|
+
\$fileName.EndsWith('.' + \$_, [System.StringComparison]::Ordinal)
|
|
2081
|
+
}).Count -gt 0)
|
|
1886
2082
|
}
|
|
1887
2083
|
} else {
|
|
1888
2084
|
\$items = Get-ChildItem @forceParam -Path \$globPath -ErrorAction SilentlyContinue
|
|
@@ -1897,7 +2093,10 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
|
|
|
1897
2093
|
\$extList = (\$extensions -split ',') -replace '^\\.',''
|
|
1898
2094
|
\$items = Get-ChildItem @forceParam -Path \$globPath -ErrorAction SilentlyContinue |
|
|
1899
2095
|
Where-Object {
|
|
1900
|
-
\$
|
|
2096
|
+
\$fileName = \$_.Name
|
|
2097
|
+
\$_.PSIsContainer -or (@(\$extList | Where-Object {
|
|
2098
|
+
\$fileName.EndsWith('.' + \$_, [System.StringComparison]::Ordinal)
|
|
2099
|
+
}).Count -gt 0)
|
|
1901
2100
|
}
|
|
1902
2101
|
} else {
|
|
1903
2102
|
\$items = Get-ChildItem @forceParam -Path \$globPath -ErrorAction SilentlyContinue
|
|
@@ -1929,15 +2128,32 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
|
|
|
1929
2128
|
\$items | ForEach-Object {
|
|
1930
2129
|
\$completionText = if (\$_.PSIsContainer) { "\$dirPrefix\$(\$_.Name)/" } else { "\$dirPrefix\$(\$_.Name)" }
|
|
1931
2130
|
\$itemType = if (\$_.PSIsContainer) { 'Directory' } else { 'File' }
|
|
2131
|
+
\$resultType = if (\$_.PSIsContainer) { 'ProviderContainer' } else { 'ParameterValue' }
|
|
2132
|
+
# PSReadLine recognizes the platform separator for containers.
|
|
2133
|
+
# Keep the display label's existing slash convention.
|
|
2134
|
+
\$directorySuffix = [IO.Path]::DirectorySeparatorChar
|
|
2135
|
+
\$pathText = if (\$_.PSIsContainer) { "\$dirPrefix\$(\$_.Name)\$directorySuffix" } else { \$completionText }
|
|
1932
2136
|
# Quoting disables native tilde expansion. A home pattern
|
|
1933
2137
|
# supplied by the parser must insert the discovered path.
|
|
1934
2138
|
\$replacementText = if (\$null -ne \$quoteType -and -not \$prefixFromInput -and \$homePattern) {
|
|
1935
|
-
if (\$_.PSIsContainer) { \$_.FullName +
|
|
1936
|
-
} else { \$
|
|
2139
|
+
if (\$_.PSIsContainer) { \$_.FullName + \$directorySuffix } else { \$_.FullName }
|
|
2140
|
+
} else { \$pathText }
|
|
2141
|
+
# Quoting suppresses native tilde expansion. Keep safe
|
|
2142
|
+
# ~/ paths bare; otherwise use the enumerated full path.
|
|
2143
|
+
\$insertionText = if (\$null -ne \$quoteType) {
|
|
2144
|
+
& \$quoteCompletion \$replacementText
|
|
2145
|
+
} elseif (\$completionText -cmatch '\\A~/[a-zA-Z0-9_./:\\\\-]*\\z') {
|
|
2146
|
+
\$pathText
|
|
2147
|
+
} else {
|
|
2148
|
+
\$argumentText = if (\$completionText.StartsWith('~/') -or \$completionText.StartsWith('~\\')) {
|
|
2149
|
+
if (\$_.PSIsContainer) { \$_.FullName + \$directorySuffix } else { \$_.FullName }
|
|
2150
|
+
} else { \$pathText }
|
|
2151
|
+
& \$quoteArgument \$argumentText
|
|
2152
|
+
}
|
|
1937
2153
|
[System.Management.Automation.CompletionResult]::new(
|
|
1938
|
-
|
|
2154
|
+
\$insertionText,
|
|
1939
2155
|
\$completionText,
|
|
1940
|
-
|
|
2156
|
+
\$resultType,
|
|
1941
2157
|
\$itemType
|
|
1942
2158
|
)
|
|
1943
2159
|
}
|
package/dist/completion.js
CHANGED
|
@@ -120,7 +120,7 @@ function _${programName} () {
|
|
|
120
120
|
esac
|
|
121
121
|
fi
|
|
122
122
|
done
|
|
123
|
-
local __tilde_pattern='^(~[a-zA-Z0-9_.+-]*)(/.*)
|
|
123
|
+
local __tilde_pattern='^(~[a-zA-Z0-9_.+-]*)(/.*)?$'
|
|
124
124
|
local __tilde_glob_pattern='^(~[a-zA-Z0-9_.+-]*)(/.*)?$'
|
|
125
125
|
local prev=("\${COMP_WORDS[@]:1:COMP_CWORD-1}")
|
|
126
126
|
# Bash splits unquoted '=' at its default word breaks. Reassemble only
|
|
@@ -353,8 +353,9 @@ function _${programName} () {
|
|
|
353
353
|
|
|
354
354
|
# path() echoes the raw shell word as its pattern. Once dequoted,
|
|
355
355
|
# use the literal current-word prefix instead of the escaped pattern.
|
|
356
|
-
if [[
|
|
357
|
-
|
|
356
|
+
if [[ "$pattern" == "$__raw_file_current" && "$__file_current" != "$__raw_file_current" ]] ||
|
|
357
|
+
{ [[ -n "$__reply_prefix" ]] && [ "$pattern" = "$__file_current" ] &&
|
|
358
|
+
{ [[ "$pattern" != *[\*\?]* ]] || [ "$__raw_file_current" != "$__file_current" ]; }; }; then
|
|
358
359
|
pattern=""
|
|
359
360
|
fi
|
|
360
361
|
|
|
@@ -388,7 +389,8 @@ function _${programName} () {
|
|
|
388
389
|
# However, if the user has already typed beyond the pattern (e.g.,
|
|
389
390
|
# pattern="src/" and current="src/ma"), preserve the typed suffix
|
|
390
391
|
# for incremental filtering.
|
|
391
|
-
local __from_pattern=0
|
|
392
|
+
local __from_pattern=0 __filter_pattern=0 __pattern_prefix=""
|
|
393
|
+
local __pattern_tilde_prefix="" __pattern_tilde_expanded=""
|
|
392
394
|
if [[ -n "$pattern" ]]; then
|
|
393
395
|
# Normalize leading ./ so that ./src/ma matches pattern src/
|
|
394
396
|
local __norm_current="\${__file_current#./}"
|
|
@@ -400,10 +402,27 @@ function _${programName} () {
|
|
|
400
402
|
__compare_pattern="\${__compare_pattern%%[\*\?]*}"
|
|
401
403
|
[[ "$__compare_pattern" == */* ]] && __compare_pattern="\${__compare_pattern%/*}/" || __compare_pattern=""
|
|
402
404
|
fi
|
|
405
|
+
local __extends_pattern=0
|
|
403
406
|
if [[ ( -n "$__compare_pattern" || -n "$__norm_current" ) && \${#__norm_current} -ge \${#__compare_pattern} && "\${__norm_current:0:\${#__compare_pattern}}" == "$__compare_pattern" && "$__file_current" != "$pattern" ]]; then
|
|
407
|
+
__extends_pattern=1
|
|
408
|
+
fi
|
|
409
|
+
if (( __extends_pattern )) && [[ "$__norm_pattern" != *[\*\?]* ]]; then
|
|
404
410
|
# User has typed beyond or an equivalent form of the pattern
|
|
405
411
|
true
|
|
406
412
|
else
|
|
413
|
+
# Wildcards keep their file filter while the typed prefix narrows
|
|
414
|
+
# the resulting paths. Save the expanded prefix before using pattern.
|
|
415
|
+
if [[ "$__norm_pattern" == *[\*\?]* ]]; then
|
|
416
|
+
__filter_pattern=1
|
|
417
|
+
__pattern_prefix="$__glob_current"
|
|
418
|
+
# An unquoted value equal to its wildcard is a complete search
|
|
419
|
+
# pattern, not a literal filename prefix to filter against.
|
|
420
|
+
if [ "$__file_current" = "$pattern" ] && [ "$__raw_file_current" = "$__file_current" ]; then
|
|
421
|
+
__pattern_prefix=""
|
|
422
|
+
fi
|
|
423
|
+
__pattern_tilde_prefix="$__tilde_prefix"
|
|
424
|
+
__pattern_tilde_expanded="$__tilde_expanded"
|
|
425
|
+
fi
|
|
407
426
|
__from_pattern=1
|
|
408
427
|
# Reset tilde state from the current-word expansion so that a
|
|
409
428
|
# non-tilde pattern is not rewritten through stale tilde state
|
|
@@ -432,36 +451,93 @@ function _${programName} () {
|
|
|
432
451
|
# This runs after tilde expansion so that paths like ~/.config/ are
|
|
433
452
|
# checked against the expanded path, not the literal ~ string.
|
|
434
453
|
local __inside_hidden_path=0
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
454
|
+
local __hidden_directory_pattern='/[.]([^./][^/]*|[.][^/]+)/'
|
|
455
|
+
if [[ "/\${__glob_current%/}/" =~ $__hidden_directory_pattern ||
|
|
456
|
+
"/\${__pattern_prefix%/}/" =~ $__hidden_directory_pattern ]]; then
|
|
457
|
+
__inside_hidden_path=1
|
|
458
|
+
fi
|
|
438
459
|
# Also check if the current prefix explicitly targets hidden entries
|
|
439
460
|
# (e.g., user typed "." or ".e" to complete .env)
|
|
440
461
|
local __prefix_targets_hidden=0
|
|
441
462
|
local __prefix_base="\${__glob_current##*/}"
|
|
463
|
+
if [[ -n "$__pattern_prefix" ]]; then __prefix_base="\${__pattern_prefix##*/}"; fi
|
|
442
464
|
[[ "$__prefix_base" == .* ]] && __prefix_targets_hidden=1
|
|
443
465
|
if [[ "$hidden" == "1" || "$__inside_hidden_path" == "1" || "$__prefix_targets_hidden" == "1" ]]; then shopt -s dotglob; fi
|
|
444
466
|
|
|
445
467
|
# Pre-expand file candidates. When the glob base came from the
|
|
446
468
|
# program's pattern and contains wildcard characters (* or ?),
|
|
447
|
-
#
|
|
469
|
+
# expand it as data with shell globbing. Empty IFS avoids splitting paths;
|
|
470
|
+
# NUL records preserve spaces and newlines without requiring mapfile.
|
|
471
|
+
# Shell expansion respects dotglob on older Bash versions as well.
|
|
448
472
|
# Otherwise append * to treat it as a prefix.
|
|
449
|
-
#
|
|
450
|
-
#
|
|
473
|
+
# Only * and ? are wildcard syntax; escape brackets, backslashes, and
|
|
474
|
+
# extended-glob operators so other pattern characters stay literal.
|
|
451
475
|
local -a __candidates=()
|
|
452
476
|
if [[ "$__from_pattern" == "1" && "$__glob_current" == *[\*\?]* ]]; then
|
|
453
|
-
|
|
477
|
+
local __pathname_glob="" __glob_char __glob_index
|
|
478
|
+
for ((__glob_index=0; __glob_index<\${#__glob_current}; __glob_index++)); do
|
|
479
|
+
__glob_char="\${__glob_current:__glob_index:1}"
|
|
480
|
+
case "$__glob_char" in
|
|
481
|
+
'['|']'|'\\'|'('|')'|'|'|'+'|'@'|'!')
|
|
482
|
+
__pathname_glob+="\\\\$__glob_char" ;;
|
|
483
|
+
*) __pathname_glob+="$__glob_char" ;;
|
|
484
|
+
esac
|
|
485
|
+
done
|
|
486
|
+
[[ "$type" == "directory" ]] && __pathname_glob="\${__pathname_glob%/}"
|
|
487
|
+
local __glob_dir="\${__pathname_glob%/*}"
|
|
488
|
+
[[ "$__glob_dir" == "$__pathname_glob" ]] && __glob_dir="."
|
|
489
|
+
[[ -z "$__glob_dir" ]] && __glob_dir="/"
|
|
490
|
+
local __navigation_dir="" __basename_glob="\${__pathname_glob##*/}"
|
|
491
|
+
# Once an offered directory is entered, keep the basename filter but
|
|
492
|
+
# enumerate there. Only descendants of matching pattern bases qualify.
|
|
493
|
+
if [[ "$__pattern_prefix" == */* ]]; then
|
|
494
|
+
local __typed_dir="\${__pattern_prefix%/*}" __base_dir __norm_base __norm_dir
|
|
495
|
+
# Resolve directory ancestry in subshells, preserving the typed spelling
|
|
496
|
+
# for insertion. The trailing slash also preserves newline-ending names.
|
|
497
|
+
if __norm_dir=$(CDPATH= builtin cd -P -- "$__typed_dir" >/dev/null 2>&1 && printf '%s/' "\${PWD%/}"); then
|
|
498
|
+
while IFS= read -r -d '' __base_dir; do
|
|
499
|
+
[[ -d "$__base_dir" ]] || continue
|
|
500
|
+
__norm_base=$(CDPATH= builtin cd -P -- "$__base_dir" >/dev/null 2>&1 && printf '%s/' "\${PWD%/}") || continue
|
|
501
|
+
if [ "\${__norm_dir:0:\${#__norm_base}}" = "$__norm_base" ]; then
|
|
502
|
+
__navigation_dir="$__typed_dir"
|
|
503
|
+
break
|
|
504
|
+
fi
|
|
505
|
+
done < <(IFS=; printf '%s\\0' $__glob_dir)
|
|
506
|
+
fi
|
|
507
|
+
fi
|
|
508
|
+
local __candidate
|
|
509
|
+
while IFS= read -r -d '' __candidate; do
|
|
510
|
+
if [[ -e "$__candidate" || -L "$__candidate" ]]; then
|
|
511
|
+
[[ -d "$__candidate" ]] && __candidate="\${__candidate%/}"
|
|
512
|
+
__candidates+=("$__candidate")
|
|
513
|
+
fi
|
|
514
|
+
done < <(
|
|
515
|
+
IFS=
|
|
516
|
+
if [[ -n "$__navigation_dir" ]]; then
|
|
517
|
+
printf '%s\\0' "$__navigation_dir"/$__basename_glob
|
|
518
|
+
else
|
|
519
|
+
printf '%s\\0' $__pathname_glob
|
|
520
|
+
fi
|
|
521
|
+
)
|
|
454
522
|
# For file/any modes, also include directories from the base
|
|
455
523
|
# directory for navigation even when the glob itself does not
|
|
456
524
|
# match directory names. Skip this for directory mode so that
|
|
457
525
|
# the pattern's basename filter is respected.
|
|
458
526
|
if [[ "$type" != "directory" ]]; then
|
|
459
|
-
local __glob_dir="\${__glob_current%/*}"
|
|
460
|
-
[[ "$__glob_dir" == "$__glob_current" ]] && __glob_dir="."
|
|
461
527
|
local __d
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
528
|
+
local __directory_pattern="\${__glob_dir%/}/*/"
|
|
529
|
+
while IFS= read -r -d '' __d; do
|
|
530
|
+
[[ -d "$__d" ]] || continue
|
|
531
|
+
__d="\${__d%/}"
|
|
532
|
+
__candidates+=("$__d")
|
|
533
|
+
done < <(
|
|
534
|
+
IFS=
|
|
535
|
+
if [[ -n "$__navigation_dir" ]]; then
|
|
536
|
+
printf '%s\\0' "$__navigation_dir"/*/
|
|
537
|
+
else
|
|
538
|
+
printf '%s\\0' $__directory_pattern
|
|
539
|
+
fi
|
|
540
|
+
)
|
|
465
541
|
fi
|
|
466
542
|
else
|
|
467
543
|
__candidates=("$__glob_current"*)
|
|
@@ -472,6 +548,26 @@ function _${programName} () {
|
|
|
472
548
|
fi
|
|
473
549
|
fi
|
|
474
550
|
|
|
551
|
+
if (( __filter_pattern )); then
|
|
552
|
+
local __nocasematch_was_set=0
|
|
553
|
+
shopt -q nocasematch && __nocasematch_was_set=1
|
|
554
|
+
if shopt -q nocaseglob; then shopt -s nocasematch; else shopt -u nocasematch; fi
|
|
555
|
+
local -a __filtered_candidates=()
|
|
556
|
+
for __candidate in "\${__candidates[@]}"; do
|
|
557
|
+
if [[ "$__candidate" == "$__pattern_prefix"* ||
|
|
558
|
+
"\${__candidate#./}" == "\${__pattern_prefix#./}"* ]]; then
|
|
559
|
+
if [[ "$__pattern_prefix" == ./* && "$__candidate" != ./* ]]; then
|
|
560
|
+
__candidate="./$__candidate"
|
|
561
|
+
elif [[ "$__pattern_prefix" != ./* && "$__pattern_prefix" != . ]]; then
|
|
562
|
+
__candidate="\${__candidate#./}"
|
|
563
|
+
fi
|
|
564
|
+
__filtered_candidates+=("$__candidate")
|
|
565
|
+
fi
|
|
566
|
+
done
|
|
567
|
+
if (( __nocasematch_was_set )); then shopt -s nocasematch; else shopt -u nocasematch; fi
|
|
568
|
+
__candidates=("\${__filtered_candidates[@]}")
|
|
569
|
+
fi
|
|
570
|
+
|
|
475
571
|
# Generate file completions based on type
|
|
476
572
|
case "$type" in
|
|
477
573
|
file)
|
|
@@ -533,6 +629,10 @@ function _${programName} () {
|
|
|
533
629
|
esac
|
|
534
630
|
|
|
535
631
|
# Restore tilde prefix in completion results
|
|
632
|
+
if (( __filter_pattern )); then
|
|
633
|
+
__tilde_prefix="$__pattern_tilde_prefix"
|
|
634
|
+
__tilde_expanded="$__pattern_tilde_expanded"
|
|
635
|
+
fi
|
|
536
636
|
if [[ -n "$__tilde_prefix" &&
|
|
537
637
|
( -z "$__quote" || $__file_quote_offset -gt 0 ) ]]; then
|
|
538
638
|
local __i
|
|
@@ -632,6 +732,31 @@ function _${programName} () {
|
|
|
632
732
|
COMPREPLY+=("\${__word:\${#__remove}}")
|
|
633
733
|
fi
|
|
634
734
|
done
|
|
735
|
+
if (( \${#COMPREPLY[@]} > 1 )) && command -v sort >/dev/null 2>&1; then
|
|
736
|
+
# Sort shell-escaped keys, not filenames: each key occupies one line even
|
|
737
|
+
# when a candidate contains tabs or newlines. Keep the first occurrence's
|
|
738
|
+
# index, then restore the original order without evaluating escaped data.
|
|
739
|
+
# This works without associative arrays or NUL-aware sort on Bash 3.2.
|
|
740
|
+
local -a __unique_replies=()
|
|
741
|
+
local __index __key __last_key=""
|
|
742
|
+
while IFS= read -r __index; do
|
|
743
|
+
__unique_replies+=("\${COMPREPLY[__index]}")
|
|
744
|
+
done < <(
|
|
745
|
+
for ((__index=0; __index<\${#COMPREPLY[@]}; __index++)); do
|
|
746
|
+
printf '%q\\t%d\\n' "\${COMPREPLY[__index]}" "$__index"
|
|
747
|
+
done |
|
|
748
|
+
LC_ALL=C sort -t $'\\t' -k1,1 -k2,2n |
|
|
749
|
+
{
|
|
750
|
+
while IFS=$'\\t' read -r __key __index; do
|
|
751
|
+
if [ "$__key" != "$__last_key" ]; then
|
|
752
|
+
printf '%s\\n' "$__index"
|
|
753
|
+
__last_key="$__key"
|
|
754
|
+
fi
|
|
755
|
+
done
|
|
756
|
+
} | LC_ALL=C sort -n
|
|
757
|
+
)
|
|
758
|
+
COMPREPLY=("\${__unique_replies[@]}")
|
|
759
|
+
fi
|
|
635
760
|
if [[ $__file_mode == 1 && $(type -t compopt) == builtin ]]; then
|
|
636
761
|
compopt -o filenames 2>/dev/null || true
|
|
637
762
|
fi
|
|
@@ -657,7 +782,7 @@ fi
|
|
|
657
782
|
}
|
|
658
783
|
for (const [index, { prefix, file }] of joined.files.entries()) {
|
|
659
784
|
if (index > 0) yield "\n";
|
|
660
|
-
yield `__JOINED_FILE__:${encodePattern(prefix)}:${file.type}:${encodeExtensions(file.extensions)}
|
|
785
|
+
yield `__JOINED_FILE__:${encodePattern(prefix)}:${file.type}:${encodeExtensions(file.extensions)}:${encodePattern(sanitizeForTransport(file.pattern ?? ""))}:${file.includeHidden ? "1" : "0"}`;
|
|
661
786
|
}
|
|
662
787
|
} else if (suggestion.kind === "literal") yield encodeBashLiteral(suggestion.text);
|
|
663
788
|
else {
|
|
@@ -679,9 +804,14 @@ const zsh = {
|
|
|
679
804
|
generateScript(programName, args = []) {
|
|
680
805
|
validateProgramName(programName);
|
|
681
806
|
const escapedArgs = args.map((arg) => `'${arg.replace(/'/g, "'\\''")}'`).join(" ");
|
|
807
|
+
const functionName = /[.-]/.test(programName) ? `_optique:${programName}` : `_${programName}`;
|
|
682
808
|
return `#compdef ${programName}
|
|
683
|
-
function
|
|
809
|
+
function ${functionName} () {
|
|
684
810
|
local current="\$words[CURRENT]"
|
|
811
|
+
# Let zsh match the suffix when completing in the middle of a word.
|
|
812
|
+
if [[ -o complete_in_word ]]; then
|
|
813
|
+
current="\$PREFIX"
|
|
814
|
+
fi
|
|
685
815
|
local -a prev
|
|
686
816
|
# Extract previous arguments, skipping empty ones
|
|
687
817
|
prev=()
|
|
@@ -769,17 +899,60 @@ function _${programName.replace(/[^a-zA-Z0-9]/g, "_")} () {
|
|
|
769
899
|
# Use zsh's native file completion
|
|
770
900
|
case "\$type" in
|
|
771
901
|
file)
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
902
|
+
# Use the same regular-file filter with or without extensions,
|
|
903
|
+
# keeping symlinked files and directory navigation available.
|
|
904
|
+
local file_pattern="\${ext_pattern:-*}(#q-.)"
|
|
905
|
+
local file_subtree_pattern="**/*(#q/N)"
|
|
906
|
+
local -a expl=() file_subtree=() file_recursive_patterns=()
|
|
907
|
+
local -a file_tags=(files directories)
|
|
908
|
+
local file_recursive_pattern file_directory file_prefix file_tag
|
|
909
|
+
if [[ -z "\$ext_pattern" ]] &&
|
|
910
|
+
zstyle -t ":completion:\${curcontext}:" list-dirs-first; then
|
|
911
|
+
file_tags=(directories files)
|
|
782
912
|
fi
|
|
913
|
+
_tags "\${file_tags[@]}"
|
|
914
|
+
while _tags; do
|
|
915
|
+
local file_matched=0
|
|
916
|
+
for file_tag in "\${file_tags[@]}"; do
|
|
917
|
+
if [[ "\$file_tag" == directories ]]; then
|
|
918
|
+
while _next_label directories expl directory; do
|
|
919
|
+
_path_files -/ "\${expl[@]}" && file_matched=1
|
|
920
|
+
done
|
|
921
|
+
else
|
|
922
|
+
while _next_label files expl file; do
|
|
923
|
+
local -a file_ignore=() file_ignore_args=()
|
|
924
|
+
# Explicit globs bypass automatic fignore. Apply it only
|
|
925
|
+
# when the tag has not supplied its own ignore patterns.
|
|
926
|
+
if [[ -z "\$ext_pattern" && -z "\$_comp_no_ignore" && -n "\$FIGNORE" ]] && (( \${#_comp_ignore[@]} == 0 )); then
|
|
927
|
+
file_ignore=("?*\${^fignore[@]}")
|
|
928
|
+
file_ignore_args=(-F file_ignore)
|
|
929
|
+
fi
|
|
930
|
+
if _path_files -g "\${file_pattern}" "\${expl[@]}" "\${file_ignore_args[@]}"; then
|
|
931
|
+
file_matched=1
|
|
932
|
+
elif [[ -z "\$ext_pattern" && "\$PREFIX\$SUFFIX" != */* ]] &&
|
|
933
|
+
zstyle -a ":completion:\${curcontext}:files" recursive-files file_recursive_patterns; then
|
|
934
|
+
# Match _files' recursive fallback, retaining the same
|
|
935
|
+
# file filter and tag-specific ignore options.
|
|
936
|
+
for file_recursive_pattern in "\${file_recursive_patterns[@]}"; do
|
|
937
|
+
if [[ "\$PWD/" = \${~file_recursive_pattern} ]]; then
|
|
938
|
+
if (( \${#file_subtree[@]} == 0 )); then
|
|
939
|
+
file_subtree=(\${~file_subtree_pattern})
|
|
940
|
+
fi
|
|
941
|
+
for file_directory in "\${file_subtree[@]}"; do
|
|
942
|
+
file_prefix="\$PREFIX"
|
|
943
|
+
PREFIX="\$file_directory/\$PREFIX"
|
|
944
|
+
_path_files -g "\${file_pattern}" "\${expl[@]}" "\${file_ignore_args[@]}" && file_matched=1
|
|
945
|
+
PREFIX="\$file_prefix"
|
|
946
|
+
done
|
|
947
|
+
break
|
|
948
|
+
fi
|
|
949
|
+
done
|
|
950
|
+
fi
|
|
951
|
+
done
|
|
952
|
+
fi
|
|
953
|
+
done
|
|
954
|
+
(( file_matched )) && break
|
|
955
|
+
done
|
|
783
956
|
;;
|
|
784
957
|
directory)
|
|
785
958
|
_directories
|
|
@@ -829,7 +1002,13 @@ function _${programName.replace(/[^a-zA-Z0-9]/g, "_")} () {
|
|
|
829
1002
|
}
|
|
830
1003
|
|
|
831
1004
|
if (( $+functions[compdef] )); then
|
|
832
|
-
compdef
|
|
1005
|
+
compdef ${functionName} ${programName}
|
|
1006
|
+
fi
|
|
1007
|
+
|
|
1008
|
+
# An autoloaded file runs as a function body. Dispatch its initial request;
|
|
1009
|
+
# sourcing the file only defines and registers the completion helper.
|
|
1010
|
+
if [[ \$ZSH_EVAL_CONTEXT == *:loadautofunc || \$ZSH_EVAL_CONTEXT == *:shfunc ]]; then
|
|
1011
|
+
${functionName} "\$@"
|
|
833
1012
|
fi
|
|
834
1013
|
`;
|
|
835
1014
|
},
|
|
@@ -855,7 +1034,7 @@ const fish = {
|
|
|
855
1034
|
generateScript(programName, args = []) {
|
|
856
1035
|
validateProgramName(programName);
|
|
857
1036
|
const escapedArgs = args.map((arg) => `'${arg.replace(/'/g, "\\'")}'`).join(" ");
|
|
858
|
-
const functionName = `__${programName
|
|
1037
|
+
const functionName = `__${programName}_complete`;
|
|
859
1038
|
return `
|
|
860
1039
|
function ${functionName}
|
|
861
1040
|
set -l tokens (commandline -poc)
|
|
@@ -1544,8 +1723,9 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
|
|
|
1544
1723
|
let all_items = (do $list_entries $ls_pattern)
|
|
1545
1724
|
let dirs = $all_items | where type == dir
|
|
1546
1725
|
let files = $all_items | where type == file | where {|f|
|
|
1547
|
-
|
|
1548
|
-
|
|
1726
|
+
$ext_list | any {|ext|
|
|
1727
|
+
$f.name | path basename | str ends-with ('.' + $ext)
|
|
1728
|
+
}
|
|
1549
1729
|
}
|
|
1550
1730
|
$dirs | append $files
|
|
1551
1731
|
}
|
|
@@ -1561,8 +1741,9 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
|
|
|
1561
1741
|
let all_items = (do $list_entries $ls_pattern)
|
|
1562
1742
|
let dirs = $all_items | where type == dir
|
|
1563
1743
|
let files = $all_items | where type == file | where {|f|
|
|
1564
|
-
|
|
1565
|
-
|
|
1744
|
+
$ext_list | any {|ext|
|
|
1745
|
+
$f.name | path basename | str ends-with ('.' + $ext)
|
|
1746
|
+
}
|
|
1566
1747
|
}
|
|
1567
1748
|
$dirs | append $files
|
|
1568
1749
|
}
|
|
@@ -1730,6 +1911,15 @@ const pwsh = {
|
|
|
1730
1911
|
Register-ArgumentCompleter -Native -CommandName ${programName} -ScriptBlock {
|
|
1731
1912
|
param(\$wordToComplete, \$commandAst, \$cursorPosition)
|
|
1732
1913
|
|
|
1914
|
+
# CompletionText is shell syntax; display labels remain unquoted.
|
|
1915
|
+
\$quoteArgument = {
|
|
1916
|
+
param([string]\$value)
|
|
1917
|
+
if (\$value -cmatch '\\A[a-zA-Z0-9_./:\\\\-]+\\z' -and \$value -cnotmatch '\\A-(?!-).*[.:]' -and \$value -cnotmatch '\\A-?(?:[0-9]|\\.[0-9])') {
|
|
1918
|
+
return \$value
|
|
1919
|
+
}
|
|
1920
|
+
return "'" + [System.Management.Automation.Language.CodeGeneration]::EscapeSingleQuotedStringContent(\$value) + "'"
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1733
1923
|
# Extract arguments from AST (handles quoted strings properly)
|
|
1734
1924
|
\$arguments = @()
|
|
1735
1925
|
\$commandElements = \$commandAst.CommandElements
|
|
@@ -1743,8 +1933,11 @@ Register-ArgumentCompleter -Native -CommandName ${programName} -ScriptBlock {
|
|
|
1743
1933
|
if (\$element.Extent.EndOffset -ge \$cursorPosition) {
|
|
1744
1934
|
# wordToComplete has synthesized delimiters and decoded escapes.
|
|
1745
1935
|
# Only the AST retains the literal value and original quote type.
|
|
1936
|
+
# PSReadLine leaves the cursor before a directory's closing quote.
|
|
1746
1937
|
if (\$element -is [System.Management.Automation.Language.StringConstantExpressionAst] -and
|
|
1747
|
-
\$element.Extent.EndOffset -eq \$cursorPosition
|
|
1938
|
+
(\$element.Extent.EndOffset -eq \$cursorPosition -or
|
|
1939
|
+
(\$element.Extent.EndOffset -eq (\$cursorPosition + 1) -and
|
|
1940
|
+
\$element.StringConstantType -in @('SingleQuoted', 'DoubleQuoted')))) {
|
|
1748
1941
|
if (\$element.StringConstantType -in @('SingleQuoted', 'DoubleQuoted')) {
|
|
1749
1942
|
\$valuePrefix = \$element.Value
|
|
1750
1943
|
\$quoteType = \$element.StringConstantType
|
|
@@ -1777,7 +1970,7 @@ Register-ArgumentCompleter -Native -CommandName ${programName} -ScriptBlock {
|
|
|
1777
1970
|
} elseif (\$quoteType -eq 'DoubleQuoted') {
|
|
1778
1971
|
'"' + (\$text -replace '[\\x60\$"\\u201c-\\u201e]', '\`\$0') + '"'
|
|
1779
1972
|
} else {
|
|
1780
|
-
\$text
|
|
1973
|
+
& \$quoteArgument \$text
|
|
1781
1974
|
}
|
|
1782
1975
|
}
|
|
1783
1976
|
|
|
@@ -1882,7 +2075,10 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
|
|
|
1882
2075
|
\$extList = (\$extensions -split ',') -replace '^\\.',''
|
|
1883
2076
|
\$items = Get-ChildItem @forceParam -Path \$globPath -ErrorAction SilentlyContinue |
|
|
1884
2077
|
Where-Object {
|
|
1885
|
-
\$
|
|
2078
|
+
\$fileName = \$_.Name
|
|
2079
|
+
\$_.PSIsContainer -or (@(\$extList | Where-Object {
|
|
2080
|
+
\$fileName.EndsWith('.' + \$_, [System.StringComparison]::Ordinal)
|
|
2081
|
+
}).Count -gt 0)
|
|
1886
2082
|
}
|
|
1887
2083
|
} else {
|
|
1888
2084
|
\$items = Get-ChildItem @forceParam -Path \$globPath -ErrorAction SilentlyContinue
|
|
@@ -1897,7 +2093,10 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
|
|
|
1897
2093
|
\$extList = (\$extensions -split ',') -replace '^\\.',''
|
|
1898
2094
|
\$items = Get-ChildItem @forceParam -Path \$globPath -ErrorAction SilentlyContinue |
|
|
1899
2095
|
Where-Object {
|
|
1900
|
-
\$
|
|
2096
|
+
\$fileName = \$_.Name
|
|
2097
|
+
\$_.PSIsContainer -or (@(\$extList | Where-Object {
|
|
2098
|
+
\$fileName.EndsWith('.' + \$_, [System.StringComparison]::Ordinal)
|
|
2099
|
+
}).Count -gt 0)
|
|
1901
2100
|
}
|
|
1902
2101
|
} else {
|
|
1903
2102
|
\$items = Get-ChildItem @forceParam -Path \$globPath -ErrorAction SilentlyContinue
|
|
@@ -1929,15 +2128,32 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
|
|
|
1929
2128
|
\$items | ForEach-Object {
|
|
1930
2129
|
\$completionText = if (\$_.PSIsContainer) { "\$dirPrefix\$(\$_.Name)/" } else { "\$dirPrefix\$(\$_.Name)" }
|
|
1931
2130
|
\$itemType = if (\$_.PSIsContainer) { 'Directory' } else { 'File' }
|
|
2131
|
+
\$resultType = if (\$_.PSIsContainer) { 'ProviderContainer' } else { 'ParameterValue' }
|
|
2132
|
+
# PSReadLine recognizes the platform separator for containers.
|
|
2133
|
+
# Keep the display label's existing slash convention.
|
|
2134
|
+
\$directorySuffix = [IO.Path]::DirectorySeparatorChar
|
|
2135
|
+
\$pathText = if (\$_.PSIsContainer) { "\$dirPrefix\$(\$_.Name)\$directorySuffix" } else { \$completionText }
|
|
1932
2136
|
# Quoting disables native tilde expansion. A home pattern
|
|
1933
2137
|
# supplied by the parser must insert the discovered path.
|
|
1934
2138
|
\$replacementText = if (\$null -ne \$quoteType -and -not \$prefixFromInput -and \$homePattern) {
|
|
1935
|
-
if (\$_.PSIsContainer) { \$_.FullName +
|
|
1936
|
-
} else { \$
|
|
2139
|
+
if (\$_.PSIsContainer) { \$_.FullName + \$directorySuffix } else { \$_.FullName }
|
|
2140
|
+
} else { \$pathText }
|
|
2141
|
+
# Quoting suppresses native tilde expansion. Keep safe
|
|
2142
|
+
# ~/ paths bare; otherwise use the enumerated full path.
|
|
2143
|
+
\$insertionText = if (\$null -ne \$quoteType) {
|
|
2144
|
+
& \$quoteCompletion \$replacementText
|
|
2145
|
+
} elseif (\$completionText -cmatch '\\A~/[a-zA-Z0-9_./:\\\\-]*\\z') {
|
|
2146
|
+
\$pathText
|
|
2147
|
+
} else {
|
|
2148
|
+
\$argumentText = if (\$completionText.StartsWith('~/') -or \$completionText.StartsWith('~\\')) {
|
|
2149
|
+
if (\$_.PSIsContainer) { \$_.FullName + \$directorySuffix } else { \$_.FullName }
|
|
2150
|
+
} else { \$pathText }
|
|
2151
|
+
& \$quoteArgument \$argumentText
|
|
2152
|
+
}
|
|
1937
2153
|
[System.Management.Automation.CompletionResult]::new(
|
|
1938
|
-
|
|
2154
|
+
\$insertionText,
|
|
1939
2155
|
\$completionText,
|
|
1940
|
-
|
|
2156
|
+
\$resultType,
|
|
1941
2157
|
\$itemType
|
|
1942
2158
|
)
|
|
1943
2159
|
}
|
|
@@ -16,10 +16,13 @@ function joinedFileSuggestion(prefix, file, value) {
|
|
|
16
16
|
text: prefix + (file.pattern ?? ""),
|
|
17
17
|
description: file.description
|
|
18
18
|
};
|
|
19
|
-
|
|
19
|
+
joinedFiles.set(literal, {
|
|
20
20
|
files: [{
|
|
21
21
|
prefix,
|
|
22
|
-
file
|
|
22
|
+
file: file.pattern === value && !/[?*]/.test(value) ? {
|
|
23
|
+
...file,
|
|
24
|
+
pattern: void 0
|
|
25
|
+
} : file
|
|
23
26
|
}],
|
|
24
27
|
literal: false
|
|
25
28
|
});
|
|
@@ -15,10 +15,13 @@ function joinedFileSuggestion(prefix, file, value) {
|
|
|
15
15
|
text: prefix + (file.pattern ?? ""),
|
|
16
16
|
description: file.description
|
|
17
17
|
};
|
|
18
|
-
|
|
18
|
+
joinedFiles.set(literal, {
|
|
19
19
|
files: [{
|
|
20
20
|
prefix,
|
|
21
|
-
file
|
|
21
|
+
file: file.pattern === value && !/[?*]/.test(value) ? {
|
|
22
|
+
...file,
|
|
23
|
+
pattern: void 0
|
|
24
|
+
} : file
|
|
22
25
|
}],
|
|
23
26
|
literal: false
|
|
24
27
|
});
|