@optique/core 1.0.15-dev.2854 → 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 {
|
|
@@ -683,6 +808,10 @@ const zsh = {
|
|
|
683
808
|
return `#compdef ${programName}
|
|
684
809
|
function ${functionName} () {
|
|
685
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
|
|
686
815
|
local -a prev
|
|
687
816
|
# Extract previous arguments, skipping empty ones
|
|
688
817
|
prev=()
|
|
@@ -770,17 +899,60 @@ function ${functionName} () {
|
|
|
770
899
|
# Use zsh's native file completion
|
|
771
900
|
case "\$type" in
|
|
772
901
|
file)
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
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)
|
|
783
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
|
|
784
956
|
;;
|
|
785
957
|
directory)
|
|
786
958
|
_directories
|
|
@@ -1551,8 +1723,9 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
|
|
|
1551
1723
|
let all_items = (do $list_entries $ls_pattern)
|
|
1552
1724
|
let dirs = $all_items | where type == dir
|
|
1553
1725
|
let files = $all_items | where type == file | where {|f|
|
|
1554
|
-
|
|
1555
|
-
|
|
1726
|
+
$ext_list | any {|ext|
|
|
1727
|
+
$f.name | path basename | str ends-with ('.' + $ext)
|
|
1728
|
+
}
|
|
1556
1729
|
}
|
|
1557
1730
|
$dirs | append $files
|
|
1558
1731
|
}
|
|
@@ -1568,8 +1741,9 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
|
|
|
1568
1741
|
let all_items = (do $list_entries $ls_pattern)
|
|
1569
1742
|
let dirs = $all_items | where type == dir
|
|
1570
1743
|
let files = $all_items | where type == file | where {|f|
|
|
1571
|
-
|
|
1572
|
-
|
|
1744
|
+
$ext_list | any {|ext|
|
|
1745
|
+
$f.name | path basename | str ends-with ('.' + $ext)
|
|
1746
|
+
}
|
|
1573
1747
|
}
|
|
1574
1748
|
$dirs | append $files
|
|
1575
1749
|
}
|
|
@@ -1737,6 +1911,15 @@ const pwsh = {
|
|
|
1737
1911
|
Register-ArgumentCompleter -Native -CommandName ${programName} -ScriptBlock {
|
|
1738
1912
|
param(\$wordToComplete, \$commandAst, \$cursorPosition)
|
|
1739
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
|
+
|
|
1740
1923
|
# Extract arguments from AST (handles quoted strings properly)
|
|
1741
1924
|
\$arguments = @()
|
|
1742
1925
|
\$commandElements = \$commandAst.CommandElements
|
|
@@ -1750,8 +1933,11 @@ Register-ArgumentCompleter -Native -CommandName ${programName} -ScriptBlock {
|
|
|
1750
1933
|
if (\$element.Extent.EndOffset -ge \$cursorPosition) {
|
|
1751
1934
|
# wordToComplete has synthesized delimiters and decoded escapes.
|
|
1752
1935
|
# Only the AST retains the literal value and original quote type.
|
|
1936
|
+
# PSReadLine leaves the cursor before a directory's closing quote.
|
|
1753
1937
|
if (\$element -is [System.Management.Automation.Language.StringConstantExpressionAst] -and
|
|
1754
|
-
\$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')))) {
|
|
1755
1941
|
if (\$element.StringConstantType -in @('SingleQuoted', 'DoubleQuoted')) {
|
|
1756
1942
|
\$valuePrefix = \$element.Value
|
|
1757
1943
|
\$quoteType = \$element.StringConstantType
|
|
@@ -1784,7 +1970,7 @@ Register-ArgumentCompleter -Native -CommandName ${programName} -ScriptBlock {
|
|
|
1784
1970
|
} elseif (\$quoteType -eq 'DoubleQuoted') {
|
|
1785
1971
|
'"' + (\$text -replace '[\\x60\$"\\u201c-\\u201e]', '\`\$0') + '"'
|
|
1786
1972
|
} else {
|
|
1787
|
-
\$text
|
|
1973
|
+
& \$quoteArgument \$text
|
|
1788
1974
|
}
|
|
1789
1975
|
}
|
|
1790
1976
|
|
|
@@ -1889,7 +2075,10 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
|
|
|
1889
2075
|
\$extList = (\$extensions -split ',') -replace '^\\.',''
|
|
1890
2076
|
\$items = Get-ChildItem @forceParam -Path \$globPath -ErrorAction SilentlyContinue |
|
|
1891
2077
|
Where-Object {
|
|
1892
|
-
\$
|
|
2078
|
+
\$fileName = \$_.Name
|
|
2079
|
+
\$_.PSIsContainer -or (@(\$extList | Where-Object {
|
|
2080
|
+
\$fileName.EndsWith('.' + \$_, [System.StringComparison]::Ordinal)
|
|
2081
|
+
}).Count -gt 0)
|
|
1893
2082
|
}
|
|
1894
2083
|
} else {
|
|
1895
2084
|
\$items = Get-ChildItem @forceParam -Path \$globPath -ErrorAction SilentlyContinue
|
|
@@ -1904,7 +2093,10 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
|
|
|
1904
2093
|
\$extList = (\$extensions -split ',') -replace '^\\.',''
|
|
1905
2094
|
\$items = Get-ChildItem @forceParam -Path \$globPath -ErrorAction SilentlyContinue |
|
|
1906
2095
|
Where-Object {
|
|
1907
|
-
\$
|
|
2096
|
+
\$fileName = \$_.Name
|
|
2097
|
+
\$_.PSIsContainer -or (@(\$extList | Where-Object {
|
|
2098
|
+
\$fileName.EndsWith('.' + \$_, [System.StringComparison]::Ordinal)
|
|
2099
|
+
}).Count -gt 0)
|
|
1908
2100
|
}
|
|
1909
2101
|
} else {
|
|
1910
2102
|
\$items = Get-ChildItem @forceParam -Path \$globPath -ErrorAction SilentlyContinue
|
|
@@ -1936,15 +2128,32 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
|
|
|
1936
2128
|
\$items | ForEach-Object {
|
|
1937
2129
|
\$completionText = if (\$_.PSIsContainer) { "\$dirPrefix\$(\$_.Name)/" } else { "\$dirPrefix\$(\$_.Name)" }
|
|
1938
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 }
|
|
1939
2136
|
# Quoting disables native tilde expansion. A home pattern
|
|
1940
2137
|
# supplied by the parser must insert the discovered path.
|
|
1941
2138
|
\$replacementText = if (\$null -ne \$quoteType -and -not \$prefixFromInput -and \$homePattern) {
|
|
1942
|
-
if (\$_.PSIsContainer) { \$_.FullName +
|
|
1943
|
-
} 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
|
+
}
|
|
1944
2153
|
[System.Management.Automation.CompletionResult]::new(
|
|
1945
|
-
|
|
2154
|
+
\$insertionText,
|
|
1946
2155
|
\$completionText,
|
|
1947
|
-
|
|
2156
|
+
\$resultType,
|
|
1948
2157
|
\$itemType
|
|
1949
2158
|
)
|
|
1950
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 {
|
|
@@ -683,6 +808,10 @@ const zsh = {
|
|
|
683
808
|
return `#compdef ${programName}
|
|
684
809
|
function ${functionName} () {
|
|
685
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
|
|
686
815
|
local -a prev
|
|
687
816
|
# Extract previous arguments, skipping empty ones
|
|
688
817
|
prev=()
|
|
@@ -770,17 +899,60 @@ function ${functionName} () {
|
|
|
770
899
|
# Use zsh's native file completion
|
|
771
900
|
case "\$type" in
|
|
772
901
|
file)
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
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)
|
|
783
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
|
|
784
956
|
;;
|
|
785
957
|
directory)
|
|
786
958
|
_directories
|
|
@@ -1551,8 +1723,9 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
|
|
|
1551
1723
|
let all_items = (do $list_entries $ls_pattern)
|
|
1552
1724
|
let dirs = $all_items | where type == dir
|
|
1553
1725
|
let files = $all_items | where type == file | where {|f|
|
|
1554
|
-
|
|
1555
|
-
|
|
1726
|
+
$ext_list | any {|ext|
|
|
1727
|
+
$f.name | path basename | str ends-with ('.' + $ext)
|
|
1728
|
+
}
|
|
1556
1729
|
}
|
|
1557
1730
|
$dirs | append $files
|
|
1558
1731
|
}
|
|
@@ -1568,8 +1741,9 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
|
|
|
1568
1741
|
let all_items = (do $list_entries $ls_pattern)
|
|
1569
1742
|
let dirs = $all_items | where type == dir
|
|
1570
1743
|
let files = $all_items | where type == file | where {|f|
|
|
1571
|
-
|
|
1572
|
-
|
|
1744
|
+
$ext_list | any {|ext|
|
|
1745
|
+
$f.name | path basename | str ends-with ('.' + $ext)
|
|
1746
|
+
}
|
|
1573
1747
|
}
|
|
1574
1748
|
$dirs | append $files
|
|
1575
1749
|
}
|
|
@@ -1737,6 +1911,15 @@ const pwsh = {
|
|
|
1737
1911
|
Register-ArgumentCompleter -Native -CommandName ${programName} -ScriptBlock {
|
|
1738
1912
|
param(\$wordToComplete, \$commandAst, \$cursorPosition)
|
|
1739
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
|
+
|
|
1740
1923
|
# Extract arguments from AST (handles quoted strings properly)
|
|
1741
1924
|
\$arguments = @()
|
|
1742
1925
|
\$commandElements = \$commandAst.CommandElements
|
|
@@ -1750,8 +1933,11 @@ Register-ArgumentCompleter -Native -CommandName ${programName} -ScriptBlock {
|
|
|
1750
1933
|
if (\$element.Extent.EndOffset -ge \$cursorPosition) {
|
|
1751
1934
|
# wordToComplete has synthesized delimiters and decoded escapes.
|
|
1752
1935
|
# Only the AST retains the literal value and original quote type.
|
|
1936
|
+
# PSReadLine leaves the cursor before a directory's closing quote.
|
|
1753
1937
|
if (\$element -is [System.Management.Automation.Language.StringConstantExpressionAst] -and
|
|
1754
|
-
\$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')))) {
|
|
1755
1941
|
if (\$element.StringConstantType -in @('SingleQuoted', 'DoubleQuoted')) {
|
|
1756
1942
|
\$valuePrefix = \$element.Value
|
|
1757
1943
|
\$quoteType = \$element.StringConstantType
|
|
@@ -1784,7 +1970,7 @@ Register-ArgumentCompleter -Native -CommandName ${programName} -ScriptBlock {
|
|
|
1784
1970
|
} elseif (\$quoteType -eq 'DoubleQuoted') {
|
|
1785
1971
|
'"' + (\$text -replace '[\\x60\$"\\u201c-\\u201e]', '\`\$0') + '"'
|
|
1786
1972
|
} else {
|
|
1787
|
-
\$text
|
|
1973
|
+
& \$quoteArgument \$text
|
|
1788
1974
|
}
|
|
1789
1975
|
}
|
|
1790
1976
|
|
|
@@ -1889,7 +2075,10 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
|
|
|
1889
2075
|
\$extList = (\$extensions -split ',') -replace '^\\.',''
|
|
1890
2076
|
\$items = Get-ChildItem @forceParam -Path \$globPath -ErrorAction SilentlyContinue |
|
|
1891
2077
|
Where-Object {
|
|
1892
|
-
\$
|
|
2078
|
+
\$fileName = \$_.Name
|
|
2079
|
+
\$_.PSIsContainer -or (@(\$extList | Where-Object {
|
|
2080
|
+
\$fileName.EndsWith('.' + \$_, [System.StringComparison]::Ordinal)
|
|
2081
|
+
}).Count -gt 0)
|
|
1893
2082
|
}
|
|
1894
2083
|
} else {
|
|
1895
2084
|
\$items = Get-ChildItem @forceParam -Path \$globPath -ErrorAction SilentlyContinue
|
|
@@ -1904,7 +2093,10 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
|
|
|
1904
2093
|
\$extList = (\$extensions -split ',') -replace '^\\.',''
|
|
1905
2094
|
\$items = Get-ChildItem @forceParam -Path \$globPath -ErrorAction SilentlyContinue |
|
|
1906
2095
|
Where-Object {
|
|
1907
|
-
\$
|
|
2096
|
+
\$fileName = \$_.Name
|
|
2097
|
+
\$_.PSIsContainer -or (@(\$extList | Where-Object {
|
|
2098
|
+
\$fileName.EndsWith('.' + \$_, [System.StringComparison]::Ordinal)
|
|
2099
|
+
}).Count -gt 0)
|
|
1908
2100
|
}
|
|
1909
2101
|
} else {
|
|
1910
2102
|
\$items = Get-ChildItem @forceParam -Path \$globPath -ErrorAction SilentlyContinue
|
|
@@ -1936,15 +2128,32 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
|
|
|
1936
2128
|
\$items | ForEach-Object {
|
|
1937
2129
|
\$completionText = if (\$_.PSIsContainer) { "\$dirPrefix\$(\$_.Name)/" } else { "\$dirPrefix\$(\$_.Name)" }
|
|
1938
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 }
|
|
1939
2136
|
# Quoting disables native tilde expansion. A home pattern
|
|
1940
2137
|
# supplied by the parser must insert the discovered path.
|
|
1941
2138
|
\$replacementText = if (\$null -ne \$quoteType -and -not \$prefixFromInput -and \$homePattern) {
|
|
1942
|
-
if (\$_.PSIsContainer) { \$_.FullName +
|
|
1943
|
-
} 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
|
+
}
|
|
1944
2153
|
[System.Management.Automation.CompletionResult]::new(
|
|
1945
|
-
|
|
2154
|
+
\$insertionText,
|
|
1946
2155
|
\$completionText,
|
|
1947
|
-
|
|
2156
|
+
\$resultType,
|
|
1948
2157
|
\$itemType
|
|
1949
2158
|
)
|
|
1950
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
|
});
|