@optique/core 1.0.15-dev.2873 → 1.0.15-dev.2883

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.
@@ -805,6 +805,7 @@ const zsh = {
805
805
  validateProgramName(programName);
806
806
  const escapedArgs = args.map((arg) => `'${arg.replace(/'/g, "'\\''")}'`).join(" ");
807
807
  const functionName = /[.-]/.test(programName) ? `_optique:${programName}` : `_${programName}`;
808
+ const fixedGlobFunction = "__optique-fixed-glob-" + Array.from(programName, (character) => character.charCodeAt(0).toString(16).padStart(2, "0")).join("");
808
809
  return `#compdef ${programName}
809
810
  function ${functionName} () {
810
811
  local current="\$words[CURRENT]"
@@ -859,6 +860,11 @@ function ${functionName} () {
859
860
  local __was_sh_glob=0
860
861
  [[ -o sh_glob ]] && __was_sh_glob=1
861
862
  unsetopt sh_glob
863
+ local __was_extended_glob=0
864
+ [[ -o extended_glob ]] && __was_extended_glob=1
865
+ local __was_sh_word_split=0
866
+ [[ -o sh_word_split ]] && __was_sh_word_split=1
867
+ unsetopt sh_word_split
862
868
 
863
869
  # Enable glob_dots when hidden files are requested so that
864
870
  # _files and _directories include dot-prefixed entries
@@ -871,7 +877,36 @@ function ${functionName} () {
871
877
  # current word. If the user has already typed beyond the pattern,
872
878
  # keep PREFIX unchanged for incremental narrowing.
873
879
  local __saved_prefix="\$PREFIX"
874
- if [[ -n "\$pattern" ]]; then
880
+ local __fixed_glob=0
881
+ local -a __fixed_matches
882
+ local __initial_file_pattern="" __initial_directory="" __initial_leaf=""
883
+ if [[ ( "\$type" == "file" || "\$type" == "any" || "\$type" == "directory" ) &&
884
+ -z "\$current\$PREFIX\$SUFFIX" &&
885
+ -z "\${compstate[pattern_match]}" &&
886
+ "\$pattern" != /* && "\$pattern" != "~"* ]]; then
887
+ if [[ "\$pattern" == */* ]]; then
888
+ __initial_directory="\${pattern%/*}/"
889
+ fi
890
+ __initial_leaf="\${pattern##*/}"
891
+ # Native -g filters are split as shell words and use extended glob
892
+ # syntax. Keep unsupported filename forms on the existing path.
893
+ if [[ "\$__initial_leaf" == *[\*\?]* &&
894
+ "\$__initial_directory" != *[\*\?]* &&
895
+ "\$__initial_directory" != *["[]()"]* &&
896
+ "\$__initial_leaf" != *["[]"]* &&
897
+ "\$__initial_leaf" != *'\\'* &&
898
+ "\$__initial_leaf" != *[[:space:]]* &&
899
+ "\$__initial_leaf" != *["#~^()|<>;&"]* &&
900
+ "\$__initial_leaf" != *"'"* && "\$__initial_leaf" != *'"'* ]]; then
901
+ __initial_file_pattern="\$__initial_leaf"
902
+ # Only the literal directory belongs in PREFIX. Quote it so
903
+ # spaces and shell metacharacters remain literal path text.
904
+ if [[ -n "\$__initial_directory" ]]; then
905
+ PREFIX="\${(q)__initial_directory}"
906
+ fi
907
+ fi
908
+ fi
909
+ if [[ -n "\$pattern" && -z "\$__initial_file_pattern" ]]; then
875
910
  # Normalize leading ./ so that ./src/ma matches pattern src/
876
911
  local __norm_prefix="\${PREFIX#./}"
877
912
  local __norm_pattern="\${pattern#./}"
@@ -881,8 +916,70 @@ function ${functionName} () {
881
916
  [[ "\$__compare_pattern" == */* ]] && __compare_pattern="\${__compare_pattern%/*}/" || __compare_pattern=""
882
917
  fi
883
918
  if [[ ( -n "\$__compare_pattern" || -n "\$__norm_prefix" ) && \${#__norm_prefix} -ge \${#__compare_pattern} && "\${__norm_prefix[1,\${#__compare_pattern}]}" == "\$__compare_pattern" && "\$PREFIX" != "\$pattern" ]]; then
884
- # User typed an equivalent or extended form — keep PREFIX
885
- true
919
+ # Keep the typed prefix, but retain a fixed file glob separately.
920
+ if [[ "\$type" != "directory" && "\$__norm_pattern" == *[\*\?]* ]]; then
921
+ __fixed_glob=1
922
+ local __fixed_was_glob_dots=0
923
+ [[ -o glob_dots ]] && __fixed_was_glob_dots=1
924
+ if [[ "\$PREFIX" == .* || "\$PREFIX" == */.* ]]; then setopt glob_dots; fi
925
+ # Keep qualifiers in a string so sourcing also works with SH_GLOB.
926
+ local __fixed_pattern="" __glob_char="" __glob_previous=""
927
+ local __glob_escaped=0
928
+ local __glob_class=0 __glob_class_start=0 __glob_class_nested=""
929
+ local -i __glob_index
930
+ for (( __glob_index=1; __glob_index <= \${#pattern}; __glob_index++ )); do
931
+ __glob_char="\${pattern[__glob_index]}"
932
+ if [[ "\$__glob_class" == "1" ]]; then
933
+ # Class members are already literal. Preserve leading ]
934
+ # and nested POSIX classes without adding backslashes.
935
+ __fixed_pattern+="\$__glob_char"
936
+ if [[ -n "\$__glob_class_nested" ]]; then
937
+ if [[ "\$__glob_char" == "]" && "\$__glob_previous" == "\$__glob_class_nested" ]]; then
938
+ __glob_class_nested=""
939
+ fi
940
+ elif [[ "\$__glob_char" == "[" && "\${pattern[__glob_index+1]}" == ":" ]]; then
941
+ __glob_class_nested="\${pattern[__glob_index+1]}"
942
+ __glob_class_start=0
943
+ elif [[ "\$__glob_char" == "]" && "\$__glob_class_start" == "0" ]]; then
944
+ __glob_class=0
945
+ elif [[ "\$__glob_class_start" == "1" && ( "\$__glob_char" == "!" || "\$__glob_char" == "^" ) &&
946
+ ( "\${pattern[__glob_index+1]}" != "]" || "\${pattern[__glob_index+2,-1]}" == *']'* ) ]]; then
947
+ __glob_class_start=2
948
+ else
949
+ __glob_class_start=0
950
+ fi
951
+ __glob_previous="\$__glob_char"
952
+ continue
953
+ fi
954
+ if [[ "\$__glob_escaped" == "1" ]]; then
955
+ __fixed_pattern+="\$__glob_char"
956
+ __glob_escaped=0
957
+ __glob_previous=""
958
+ continue
959
+ fi
960
+ if [[ "\$__glob_char" == "\\\\" ]]; then
961
+ __fixed_pattern+="\$__glob_char"
962
+ __glob_escaped=1
963
+ elif [[ "\$__glob_char" == "[" ]]; then
964
+ __fixed_pattern+="\$__glob_char"
965
+ __glob_class=1
966
+ __glob_class_start=1
967
+ elif [[ "\$__glob_char" == [#^~\\(\\)\\|\\<\\>] &&
968
+ ! ( "\$__glob_char" == "~" && "\$__glob_index" == "1" ) ]]; then
969
+ # Keep class negation and leading home expansion; escape
970
+ # extended-glob operators in literal pathname components.
971
+ __fixed_pattern+="\\\\\$__glob_char"
972
+ else
973
+ __fixed_pattern+="\$__glob_char"
974
+ fi
975
+ __glob_previous="\$__glob_char"
976
+ done
977
+ __fixed_pattern+="(#qN)"
978
+ __fixed_matches=( \${~__fixed_pattern} )
979
+ __fixed_matches=( "\${(@)__fixed_matches:a}" )
980
+ # Hidden parents widen fixed matches, not native child suggestions.
981
+ if [[ "\$__fixed_was_glob_dots" == "0" && "\${PREFIX##*/}" != .* ]]; then unsetopt glob_dots; fi
982
+ fi
886
983
  else
887
984
  PREFIX="\$pattern"
888
985
  fi
@@ -896,81 +993,111 @@ function ${functionName} () {
896
993
  ext_pattern="*.\$extensions"
897
994
  fi
898
995
  fi
996
+ # _wanted/_next_label write to the named explanation parameter.
997
+ # Keep it local for every file directive, including directory/any.
998
+ local -a expl=()
899
999
  # Use zsh's native file completion
900
1000
  case "\$type" in
901
- file)
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)
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
1001
+ file|any)
1002
+ if [[ "\$type" == "file" || -n "\$__initial_file_pattern" ]]; then
1003
+ # Regular-file suggestions retain symlinked files; initial
1004
+ # any globs use a non-directory qualifier instead.
1005
+ local file_pattern="\${ext_pattern:-*}(#q-.)"
1006
+ if [[ "\$__fixed_glob" == "1" ]]; then
1007
+ file_pattern="\${ext_pattern:-*}(#q-.e:${fixedGlobFunction}:)"
1008
+ fi
1009
+ if [[ -n "\$__initial_file_pattern" ]]; then
1010
+ local __file_qualifier="-."
1011
+ [[ "\$type" == "any" ]] && __file_qualifier="^-/"
1012
+ local -a __initial_file_patterns=()
1013
+ if [[ -n "\$extensions" ]]; then
1014
+ setopt extended_glob
1015
+ local __initial_extension
1016
+ for __initial_extension in "\${(@s:,:)extensions}"; do
1017
+ __initial_file_patterns+=("(\${__initial_file_pattern}~^*.\${__initial_extension})(#q\${__file_qualifier})")
920
1018
  done
921
1019
  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
1020
+ __initial_file_patterns=("\${__initial_file_pattern}(#q\${__file_qualifier})")
952
1021
  fi
1022
+ file_pattern="\${(j: :)__initial_file_patterns}"
1023
+ fi
1024
+ local file_subtree_pattern="**/*(#q/N)"
1025
+ local -a file_subtree=() file_recursive_patterns=()
1026
+ local -a file_tags=(files directories)
1027
+ local file_recursive_pattern file_directory file_prefix file_tag
1028
+ if [[ -n "\$__initial_file_pattern" || -z "\$ext_pattern" ]] &&
1029
+ zstyle -t ":completion:\${curcontext}:" list-dirs-first; then
1030
+ file_tags=(directories files)
1031
+ fi
1032
+ _tags "\${file_tags[@]}"
1033
+ while _tags; do
1034
+ local file_matched=0
1035
+ for file_tag in "\${file_tags[@]}"; do
1036
+ if [[ "\$file_tag" == directories ]]; then
1037
+ while _next_label directories expl directory; do
1038
+ _path_files -/ "\${expl[@]}" && file_matched=1
1039
+ done
1040
+ else
1041
+ while _next_label files expl file; do
1042
+ local -a file_ignore=() file_ignore_args=()
1043
+ # Explicit globs bypass automatic fignore. Apply it only
1044
+ # when the tag has not supplied its own ignore patterns.
1045
+ if [[ -z "\$__initial_file_pattern" && -z "\$ext_pattern" && ( "\$__fixed_glob" != "1" || "\$pattern" == "*" ) && -z "\$_comp_no_ignore" && -n "\$FIGNORE" ]] && (( \${#_comp_ignore[@]} == 0 )); then
1046
+ file_ignore=("?*\${^fignore[@]}")
1047
+ file_ignore_args=(-F file_ignore)
1048
+ fi
1049
+ if _path_files -g "\${file_pattern}" "\${expl[@]}" "\${file_ignore_args[@]}"; then
1050
+ file_matched=1
1051
+ elif [[ ( -n "\$__initial_file_pattern" || -z "\$ext_pattern" ) && "\$PREFIX\$SUFFIX" != */* ]] &&
1052
+ zstyle -a ":completion:\${curcontext}:files" recursive-files file_recursive_patterns; then
1053
+ # Match _files' recursive fallback, retaining the same
1054
+ # file filter and tag-specific ignore options.
1055
+ for file_recursive_pattern in "\${file_recursive_patterns[@]}"; do
1056
+ if [[ "\$PWD/" = \${~file_recursive_pattern} ]]; then
1057
+ if (( \${#file_subtree[@]} == 0 )); then
1058
+ file_subtree=(\${~file_subtree_pattern})
1059
+ fi
1060
+ for file_directory in "\${file_subtree[@]}"; do
1061
+ file_prefix="\$PREFIX"
1062
+ PREFIX="\$file_directory/\$PREFIX"
1063
+ _path_files -g "\${file_pattern}" "\${expl[@]}" "\${file_ignore_args[@]}" && file_matched=1
1064
+ PREFIX="\$file_prefix"
1065
+ done
1066
+ break
1067
+ fi
1068
+ done
1069
+ fi
1070
+ done
1071
+ fi
1072
+ done
1073
+ (( file_matched )) && break
953
1074
  done
954
- (( file_matched )) && break
955
- done
956
- ;;
957
- directory)
958
- _directories
959
- ;;
960
- any)
961
- if [[ -n "\$ext_pattern" ]]; then
1075
+ elif [[ -n "\$ext_pattern" || "\$__fixed_glob" == "1" ]]; then
962
1076
  local file_pattern="\${ext_pattern}(#q^-/)"
1077
+ if [[ "\$__fixed_glob" == "1" ]]; then
1078
+ # Put the predicate before ^, which negates later qualifiers.
1079
+ file_pattern="\${ext_pattern:-*}(#qe:${fixedGlobFunction}:^-/)"
1080
+ fi
963
1081
  _wanted files expl file _path_files -g "\${file_pattern}"
964
1082
  _wanted directories expl directory _path_files -/
965
1083
  else
966
1084
  _files
967
1085
  fi
968
1086
  ;;
1087
+ directory)
1088
+ if [[ -n "\$__initial_file_pattern" ]]; then
1089
+ # -/ would add every directory alongside the filtered glob.
1090
+ _wanted directories expl directory _path_files -g "\${__initial_file_pattern}(#q-/)"
1091
+ else
1092
+ _directories
1093
+ fi
1094
+ ;;
969
1095
  esac
970
-
971
1096
  # Restore PREFIX and glob_dots to their previous state
972
1097
  PREFIX="\$__saved_prefix"
1098
+ if [[ "\$__was_extended_glob" == "1" ]]; then setopt extended_glob; else unsetopt extended_glob; fi
973
1099
  if [[ "\$__was_sh_glob" == "1" ]]; then setopt sh_glob; else unsetopt sh_glob; fi
1100
+ if [[ "\$__was_sh_word_split" == "1" ]]; then setopt sh_word_split; else unsetopt sh_word_split; fi
974
1101
  if [[ "\$__was_glob_dots" == "1" ]]; then setopt glob_dots; else unsetopt glob_dots; fi
975
1102
  else
976
1103
  # Regular literal completion
@@ -1001,6 +1128,11 @@ function ${functionName} () {
1001
1128
  fi
1002
1129
  }
1003
1130
 
1131
+ function ${fixedGlobFunction} () {
1132
+ local __fixed_candidate="\${REPLY:a}"
1133
+ (( \${__fixed_matches[(Ie)\$__fixed_candidate]} ))
1134
+ }
1135
+
1004
1136
  if (( $+functions[compdef] )); then
1005
1137
  compdef ${functionName} ${programName}
1006
1138
  fi
@@ -1634,12 +1766,12 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
1634
1766
  # prefix for incremental narrowing. Normalize path separators
1635
1767
  # before comparing so that Windows backslashes match forward slashes
1636
1768
  # in the transported pattern.
1769
+ let is_win = (($nu.os-info.name | str downcase) == "windows")
1637
1770
  let glob_base = if ($pattern | is-not-empty) {
1638
1771
  # Normalize separators and leading ./; downcase only on Windows
1639
1772
  # where filesystems are typically case-insensitive
1640
1773
  let norm_prefix_raw = ($prefix | str replace -a '\\' '/' | str replace -r '^\\./' '')
1641
1774
  let norm_pattern_raw = ($pattern | str replace -a '\\' '/' | str replace -r '^\\./' '')
1642
- let is_win = (($nu.os-info.name | str downcase) == "windows")
1643
1775
  let norm_prefix = (if $is_win { $norm_prefix_raw | str downcase } else { $norm_prefix_raw })
1644
1776
  let norm_pattern = (if $is_win { $norm_pattern_raw | str downcase } else { $norm_pattern_raw })
1645
1777
  # For wildcard patterns, compare only the directory prefix
@@ -1658,6 +1790,21 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
1658
1790
  $prefix
1659
1791
  }
1660
1792
 
1793
+ # Intersect fixed glob matches with the narrowed prefix candidates.
1794
+ # Directory-only suggestions retain their existing navigation behavior.
1795
+ let narrow_fixed_glob = ($type != "directory") and ($pattern =~ '[*?]') and ($prefix != $pattern) and ($glob_base == $prefix)
1796
+ let fixed_matches = if $narrow_fixed_glob {
1797
+ try {
1798
+ let fixed_pattern = ($pattern | into glob)
1799
+ let include_hidden = $hidden or ($prefix | path basename | str starts-with '.')
1800
+ let matches = if $include_hidden { ls -a --directory $fixed_pattern } else { ls --directory $fixed_pattern }
1801
+ $matches | each {|item|
1802
+ let name = ($env.PWD | path join $item.name | path expand --no-symlink)
1803
+ if $is_win { $name | str downcase } else { $name }
1804
+ }
1805
+ } catch { [] }
1806
+ } else { [] }
1807
+
1661
1808
  # If the glob base is a directory without a trailing slash (e.g., "src"),
1662
1809
  # append one so that ls enumerates its contents instead of matching
1663
1810
  # siblings like "src-old". path expand follows symlinks, so that
@@ -1780,6 +1927,17 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
1780
1927
  }
1781
1928
  }
1782
1929
 
1930
+ let filtered = if $narrow_fixed_glob {
1931
+ $filtered | where {|item|
1932
+ # Any-mode listings already include directory symlinks; keep them
1933
+ # navigable without changing their existing completion rendering.
1934
+ let navigation_dir = ($item.type == dir) or ($item.type == symlink and (try { ($item.name | path expand | path type) == "dir" } catch { false }))
1935
+ let name = ($env.PWD | path join $item.name | path expand --no-symlink)
1936
+ let normalized_name = if $is_win { $name | str downcase } else { $name }
1937
+ $navigation_dir or ($normalized_name in $fixed_matches)
1938
+ }
1939
+ } else { $filtered }
1940
+
1783
1941
  # Extract directory prefix to preserve in completion text
1784
1942
  let dir_prefix = if ($glob_base | is-empty) {
1785
1943
  ""
@@ -2055,6 +2213,9 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
2055
2213
  \$prefixFromInput = \$valuePrefix -and \$prefix -ceq \$valuePrefix
2056
2214
  \$lookupPrefix = if (\$prefixFromInput -and \$null -ne \$quoteType -and \$prefix.StartsWith('~')) { './' + \$prefix } else { \$prefix }
2057
2215
 
2216
+ # Detect narrowing before directory normalization changes prefix.
2217
+ \$narrowFixedGlob = \$type -ne 'directory' -and \$pattern -match '[\*\?]' -and \$prefix -ceq \$valuePrefix -and \$pattern -cne \$valuePrefix
2218
+
2058
2219
  # If prefix is a directory without trailing slash, append
2059
2220
  # one so Get-ChildItem lists its contents
2060
2221
  if (\$prefix -and (Test-Path -Path \$lookupPrefix -PathType Container) -and -not \$prefix.EndsWith('/') -and -not \$prefix.EndsWith('\\')) {
@@ -2066,6 +2227,16 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
2066
2227
  # characters, use it as-is; otherwise append *
2067
2228
  \$globPath = if (\$lookupPrefix -match '[\*\?]') { \$lookupPrefix } else { "\${lookupPrefix}*" }
2068
2229
 
2230
+ # Keep the original native glob as an independent file filter
2231
+ # when the current word narrows its candidates.
2232
+ \$fixedMatches = @()
2233
+ if (\$narrowFixedGlob) {
2234
+ # Include hidden parents here; candidate enumeration still
2235
+ # controls which hidden children can be suggested.
2236
+ \$fixedMatches = @(Get-Item -Force -Path \$pattern -ErrorAction SilentlyContinue |
2237
+ Select-Object -ExpandProperty FullName)
2238
+ }
2239
+
2069
2240
  # Get file system items based on type
2070
2241
  \$items = @()
2071
2242
  switch (\$type) {
@@ -2118,6 +2289,14 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
2118
2289
  \$items = \$items | Where-Object { -not \$_.Attributes.HasFlag([System.IO.FileAttributes]::Hidden) }
2119
2290
  }
2120
2291
 
2292
+ if (\$narrowFixedGlob) {
2293
+ \$items = \$items | Where-Object {
2294
+ if (\$_.PSIsContainer) { \$true }
2295
+ elseif (\$IsWindows) { \$fixedMatches -contains \$_.FullName }
2296
+ else { \$fixedMatches -ccontains \$_.FullName }
2297
+ }
2298
+ }
2299
+
2121
2300
  # Extract directory prefix to preserve in completion text
2122
2301
  \$dirPrefix = if (\$prefix -and (\$prefix.Contains('/') -or \$prefix.Contains('\\'))) {
2123
2302
  \$slashIdx = [Math]::Max(\$prefix.LastIndexOf('/'), \$prefix.LastIndexOf('\\'))
@@ -805,6 +805,7 @@ const zsh = {
805
805
  validateProgramName(programName);
806
806
  const escapedArgs = args.map((arg) => `'${arg.replace(/'/g, "'\\''")}'`).join(" ");
807
807
  const functionName = /[.-]/.test(programName) ? `_optique:${programName}` : `_${programName}`;
808
+ const fixedGlobFunction = "__optique-fixed-glob-" + Array.from(programName, (character) => character.charCodeAt(0).toString(16).padStart(2, "0")).join("");
808
809
  return `#compdef ${programName}
809
810
  function ${functionName} () {
810
811
  local current="\$words[CURRENT]"
@@ -859,6 +860,11 @@ function ${functionName} () {
859
860
  local __was_sh_glob=0
860
861
  [[ -o sh_glob ]] && __was_sh_glob=1
861
862
  unsetopt sh_glob
863
+ local __was_extended_glob=0
864
+ [[ -o extended_glob ]] && __was_extended_glob=1
865
+ local __was_sh_word_split=0
866
+ [[ -o sh_word_split ]] && __was_sh_word_split=1
867
+ unsetopt sh_word_split
862
868
 
863
869
  # Enable glob_dots when hidden files are requested so that
864
870
  # _files and _directories include dot-prefixed entries
@@ -871,7 +877,36 @@ function ${functionName} () {
871
877
  # current word. If the user has already typed beyond the pattern,
872
878
  # keep PREFIX unchanged for incremental narrowing.
873
879
  local __saved_prefix="\$PREFIX"
874
- if [[ -n "\$pattern" ]]; then
880
+ local __fixed_glob=0
881
+ local -a __fixed_matches
882
+ local __initial_file_pattern="" __initial_directory="" __initial_leaf=""
883
+ if [[ ( "\$type" == "file" || "\$type" == "any" || "\$type" == "directory" ) &&
884
+ -z "\$current\$PREFIX\$SUFFIX" &&
885
+ -z "\${compstate[pattern_match]}" &&
886
+ "\$pattern" != /* && "\$pattern" != "~"* ]]; then
887
+ if [[ "\$pattern" == */* ]]; then
888
+ __initial_directory="\${pattern%/*}/"
889
+ fi
890
+ __initial_leaf="\${pattern##*/}"
891
+ # Native -g filters are split as shell words and use extended glob
892
+ # syntax. Keep unsupported filename forms on the existing path.
893
+ if [[ "\$__initial_leaf" == *[\*\?]* &&
894
+ "\$__initial_directory" != *[\*\?]* &&
895
+ "\$__initial_directory" != *["[]()"]* &&
896
+ "\$__initial_leaf" != *["[]"]* &&
897
+ "\$__initial_leaf" != *'\\'* &&
898
+ "\$__initial_leaf" != *[[:space:]]* &&
899
+ "\$__initial_leaf" != *["#~^()|<>;&"]* &&
900
+ "\$__initial_leaf" != *"'"* && "\$__initial_leaf" != *'"'* ]]; then
901
+ __initial_file_pattern="\$__initial_leaf"
902
+ # Only the literal directory belongs in PREFIX. Quote it so
903
+ # spaces and shell metacharacters remain literal path text.
904
+ if [[ -n "\$__initial_directory" ]]; then
905
+ PREFIX="\${(q)__initial_directory}"
906
+ fi
907
+ fi
908
+ fi
909
+ if [[ -n "\$pattern" && -z "\$__initial_file_pattern" ]]; then
875
910
  # Normalize leading ./ so that ./src/ma matches pattern src/
876
911
  local __norm_prefix="\${PREFIX#./}"
877
912
  local __norm_pattern="\${pattern#./}"
@@ -881,8 +916,70 @@ function ${functionName} () {
881
916
  [[ "\$__compare_pattern" == */* ]] && __compare_pattern="\${__compare_pattern%/*}/" || __compare_pattern=""
882
917
  fi
883
918
  if [[ ( -n "\$__compare_pattern" || -n "\$__norm_prefix" ) && \${#__norm_prefix} -ge \${#__compare_pattern} && "\${__norm_prefix[1,\${#__compare_pattern}]}" == "\$__compare_pattern" && "\$PREFIX" != "\$pattern" ]]; then
884
- # User typed an equivalent or extended form — keep PREFIX
885
- true
919
+ # Keep the typed prefix, but retain a fixed file glob separately.
920
+ if [[ "\$type" != "directory" && "\$__norm_pattern" == *[\*\?]* ]]; then
921
+ __fixed_glob=1
922
+ local __fixed_was_glob_dots=0
923
+ [[ -o glob_dots ]] && __fixed_was_glob_dots=1
924
+ if [[ "\$PREFIX" == .* || "\$PREFIX" == */.* ]]; then setopt glob_dots; fi
925
+ # Keep qualifiers in a string so sourcing also works with SH_GLOB.
926
+ local __fixed_pattern="" __glob_char="" __glob_previous=""
927
+ local __glob_escaped=0
928
+ local __glob_class=0 __glob_class_start=0 __glob_class_nested=""
929
+ local -i __glob_index
930
+ for (( __glob_index=1; __glob_index <= \${#pattern}; __glob_index++ )); do
931
+ __glob_char="\${pattern[__glob_index]}"
932
+ if [[ "\$__glob_class" == "1" ]]; then
933
+ # Class members are already literal. Preserve leading ]
934
+ # and nested POSIX classes without adding backslashes.
935
+ __fixed_pattern+="\$__glob_char"
936
+ if [[ -n "\$__glob_class_nested" ]]; then
937
+ if [[ "\$__glob_char" == "]" && "\$__glob_previous" == "\$__glob_class_nested" ]]; then
938
+ __glob_class_nested=""
939
+ fi
940
+ elif [[ "\$__glob_char" == "[" && "\${pattern[__glob_index+1]}" == ":" ]]; then
941
+ __glob_class_nested="\${pattern[__glob_index+1]}"
942
+ __glob_class_start=0
943
+ elif [[ "\$__glob_char" == "]" && "\$__glob_class_start" == "0" ]]; then
944
+ __glob_class=0
945
+ elif [[ "\$__glob_class_start" == "1" && ( "\$__glob_char" == "!" || "\$__glob_char" == "^" ) &&
946
+ ( "\${pattern[__glob_index+1]}" != "]" || "\${pattern[__glob_index+2,-1]}" == *']'* ) ]]; then
947
+ __glob_class_start=2
948
+ else
949
+ __glob_class_start=0
950
+ fi
951
+ __glob_previous="\$__glob_char"
952
+ continue
953
+ fi
954
+ if [[ "\$__glob_escaped" == "1" ]]; then
955
+ __fixed_pattern+="\$__glob_char"
956
+ __glob_escaped=0
957
+ __glob_previous=""
958
+ continue
959
+ fi
960
+ if [[ "\$__glob_char" == "\\\\" ]]; then
961
+ __fixed_pattern+="\$__glob_char"
962
+ __glob_escaped=1
963
+ elif [[ "\$__glob_char" == "[" ]]; then
964
+ __fixed_pattern+="\$__glob_char"
965
+ __glob_class=1
966
+ __glob_class_start=1
967
+ elif [[ "\$__glob_char" == [#^~\\(\\)\\|\\<\\>] &&
968
+ ! ( "\$__glob_char" == "~" && "\$__glob_index" == "1" ) ]]; then
969
+ # Keep class negation and leading home expansion; escape
970
+ # extended-glob operators in literal pathname components.
971
+ __fixed_pattern+="\\\\\$__glob_char"
972
+ else
973
+ __fixed_pattern+="\$__glob_char"
974
+ fi
975
+ __glob_previous="\$__glob_char"
976
+ done
977
+ __fixed_pattern+="(#qN)"
978
+ __fixed_matches=( \${~__fixed_pattern} )
979
+ __fixed_matches=( "\${(@)__fixed_matches:a}" )
980
+ # Hidden parents widen fixed matches, not native child suggestions.
981
+ if [[ "\$__fixed_was_glob_dots" == "0" && "\${PREFIX##*/}" != .* ]]; then unsetopt glob_dots; fi
982
+ fi
886
983
  else
887
984
  PREFIX="\$pattern"
888
985
  fi
@@ -896,81 +993,111 @@ function ${functionName} () {
896
993
  ext_pattern="*.\$extensions"
897
994
  fi
898
995
  fi
996
+ # _wanted/_next_label write to the named explanation parameter.
997
+ # Keep it local for every file directive, including directory/any.
998
+ local -a expl=()
899
999
  # Use zsh's native file completion
900
1000
  case "\$type" in
901
- file)
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)
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
1001
+ file|any)
1002
+ if [[ "\$type" == "file" || -n "\$__initial_file_pattern" ]]; then
1003
+ # Regular-file suggestions retain symlinked files; initial
1004
+ # any globs use a non-directory qualifier instead.
1005
+ local file_pattern="\${ext_pattern:-*}(#q-.)"
1006
+ if [[ "\$__fixed_glob" == "1" ]]; then
1007
+ file_pattern="\${ext_pattern:-*}(#q-.e:${fixedGlobFunction}:)"
1008
+ fi
1009
+ if [[ -n "\$__initial_file_pattern" ]]; then
1010
+ local __file_qualifier="-."
1011
+ [[ "\$type" == "any" ]] && __file_qualifier="^-/"
1012
+ local -a __initial_file_patterns=()
1013
+ if [[ -n "\$extensions" ]]; then
1014
+ setopt extended_glob
1015
+ local __initial_extension
1016
+ for __initial_extension in "\${(@s:,:)extensions}"; do
1017
+ __initial_file_patterns+=("(\${__initial_file_pattern}~^*.\${__initial_extension})(#q\${__file_qualifier})")
920
1018
  done
921
1019
  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
1020
+ __initial_file_patterns=("\${__initial_file_pattern}(#q\${__file_qualifier})")
952
1021
  fi
1022
+ file_pattern="\${(j: :)__initial_file_patterns}"
1023
+ fi
1024
+ local file_subtree_pattern="**/*(#q/N)"
1025
+ local -a file_subtree=() file_recursive_patterns=()
1026
+ local -a file_tags=(files directories)
1027
+ local file_recursive_pattern file_directory file_prefix file_tag
1028
+ if [[ -n "\$__initial_file_pattern" || -z "\$ext_pattern" ]] &&
1029
+ zstyle -t ":completion:\${curcontext}:" list-dirs-first; then
1030
+ file_tags=(directories files)
1031
+ fi
1032
+ _tags "\${file_tags[@]}"
1033
+ while _tags; do
1034
+ local file_matched=0
1035
+ for file_tag in "\${file_tags[@]}"; do
1036
+ if [[ "\$file_tag" == directories ]]; then
1037
+ while _next_label directories expl directory; do
1038
+ _path_files -/ "\${expl[@]}" && file_matched=1
1039
+ done
1040
+ else
1041
+ while _next_label files expl file; do
1042
+ local -a file_ignore=() file_ignore_args=()
1043
+ # Explicit globs bypass automatic fignore. Apply it only
1044
+ # when the tag has not supplied its own ignore patterns.
1045
+ if [[ -z "\$__initial_file_pattern" && -z "\$ext_pattern" && ( "\$__fixed_glob" != "1" || "\$pattern" == "*" ) && -z "\$_comp_no_ignore" && -n "\$FIGNORE" ]] && (( \${#_comp_ignore[@]} == 0 )); then
1046
+ file_ignore=("?*\${^fignore[@]}")
1047
+ file_ignore_args=(-F file_ignore)
1048
+ fi
1049
+ if _path_files -g "\${file_pattern}" "\${expl[@]}" "\${file_ignore_args[@]}"; then
1050
+ file_matched=1
1051
+ elif [[ ( -n "\$__initial_file_pattern" || -z "\$ext_pattern" ) && "\$PREFIX\$SUFFIX" != */* ]] &&
1052
+ zstyle -a ":completion:\${curcontext}:files" recursive-files file_recursive_patterns; then
1053
+ # Match _files' recursive fallback, retaining the same
1054
+ # file filter and tag-specific ignore options.
1055
+ for file_recursive_pattern in "\${file_recursive_patterns[@]}"; do
1056
+ if [[ "\$PWD/" = \${~file_recursive_pattern} ]]; then
1057
+ if (( \${#file_subtree[@]} == 0 )); then
1058
+ file_subtree=(\${~file_subtree_pattern})
1059
+ fi
1060
+ for file_directory in "\${file_subtree[@]}"; do
1061
+ file_prefix="\$PREFIX"
1062
+ PREFIX="\$file_directory/\$PREFIX"
1063
+ _path_files -g "\${file_pattern}" "\${expl[@]}" "\${file_ignore_args[@]}" && file_matched=1
1064
+ PREFIX="\$file_prefix"
1065
+ done
1066
+ break
1067
+ fi
1068
+ done
1069
+ fi
1070
+ done
1071
+ fi
1072
+ done
1073
+ (( file_matched )) && break
953
1074
  done
954
- (( file_matched )) && break
955
- done
956
- ;;
957
- directory)
958
- _directories
959
- ;;
960
- any)
961
- if [[ -n "\$ext_pattern" ]]; then
1075
+ elif [[ -n "\$ext_pattern" || "\$__fixed_glob" == "1" ]]; then
962
1076
  local file_pattern="\${ext_pattern}(#q^-/)"
1077
+ if [[ "\$__fixed_glob" == "1" ]]; then
1078
+ # Put the predicate before ^, which negates later qualifiers.
1079
+ file_pattern="\${ext_pattern:-*}(#qe:${fixedGlobFunction}:^-/)"
1080
+ fi
963
1081
  _wanted files expl file _path_files -g "\${file_pattern}"
964
1082
  _wanted directories expl directory _path_files -/
965
1083
  else
966
1084
  _files
967
1085
  fi
968
1086
  ;;
1087
+ directory)
1088
+ if [[ -n "\$__initial_file_pattern" ]]; then
1089
+ # -/ would add every directory alongside the filtered glob.
1090
+ _wanted directories expl directory _path_files -g "\${__initial_file_pattern}(#q-/)"
1091
+ else
1092
+ _directories
1093
+ fi
1094
+ ;;
969
1095
  esac
970
-
971
1096
  # Restore PREFIX and glob_dots to their previous state
972
1097
  PREFIX="\$__saved_prefix"
1098
+ if [[ "\$__was_extended_glob" == "1" ]]; then setopt extended_glob; else unsetopt extended_glob; fi
973
1099
  if [[ "\$__was_sh_glob" == "1" ]]; then setopt sh_glob; else unsetopt sh_glob; fi
1100
+ if [[ "\$__was_sh_word_split" == "1" ]]; then setopt sh_word_split; else unsetopt sh_word_split; fi
974
1101
  if [[ "\$__was_glob_dots" == "1" ]]; then setopt glob_dots; else unsetopt glob_dots; fi
975
1102
  else
976
1103
  # Regular literal completion
@@ -1001,6 +1128,11 @@ function ${functionName} () {
1001
1128
  fi
1002
1129
  }
1003
1130
 
1131
+ function ${fixedGlobFunction} () {
1132
+ local __fixed_candidate="\${REPLY:a}"
1133
+ (( \${__fixed_matches[(Ie)\$__fixed_candidate]} ))
1134
+ }
1135
+
1004
1136
  if (( $+functions[compdef] )); then
1005
1137
  compdef ${functionName} ${programName}
1006
1138
  fi
@@ -1634,12 +1766,12 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
1634
1766
  # prefix for incremental narrowing. Normalize path separators
1635
1767
  # before comparing so that Windows backslashes match forward slashes
1636
1768
  # in the transported pattern.
1769
+ let is_win = (($nu.os-info.name | str downcase) == "windows")
1637
1770
  let glob_base = if ($pattern | is-not-empty) {
1638
1771
  # Normalize separators and leading ./; downcase only on Windows
1639
1772
  # where filesystems are typically case-insensitive
1640
1773
  let norm_prefix_raw = ($prefix | str replace -a '\\' '/' | str replace -r '^\\./' '')
1641
1774
  let norm_pattern_raw = ($pattern | str replace -a '\\' '/' | str replace -r '^\\./' '')
1642
- let is_win = (($nu.os-info.name | str downcase) == "windows")
1643
1775
  let norm_prefix = (if $is_win { $norm_prefix_raw | str downcase } else { $norm_prefix_raw })
1644
1776
  let norm_pattern = (if $is_win { $norm_pattern_raw | str downcase } else { $norm_pattern_raw })
1645
1777
  # For wildcard patterns, compare only the directory prefix
@@ -1658,6 +1790,21 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
1658
1790
  $prefix
1659
1791
  }
1660
1792
 
1793
+ # Intersect fixed glob matches with the narrowed prefix candidates.
1794
+ # Directory-only suggestions retain their existing navigation behavior.
1795
+ let narrow_fixed_glob = ($type != "directory") and ($pattern =~ '[*?]') and ($prefix != $pattern) and ($glob_base == $prefix)
1796
+ let fixed_matches = if $narrow_fixed_glob {
1797
+ try {
1798
+ let fixed_pattern = ($pattern | into glob)
1799
+ let include_hidden = $hidden or ($prefix | path basename | str starts-with '.')
1800
+ let matches = if $include_hidden { ls -a --directory $fixed_pattern } else { ls --directory $fixed_pattern }
1801
+ $matches | each {|item|
1802
+ let name = ($env.PWD | path join $item.name | path expand --no-symlink)
1803
+ if $is_win { $name | str downcase } else { $name }
1804
+ }
1805
+ } catch { [] }
1806
+ } else { [] }
1807
+
1661
1808
  # If the glob base is a directory without a trailing slash (e.g., "src"),
1662
1809
  # append one so that ls enumerates its contents instead of matching
1663
1810
  # siblings like "src-old". path expand follows symlinks, so that
@@ -1780,6 +1927,17 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
1780
1927
  }
1781
1928
  }
1782
1929
 
1930
+ let filtered = if $narrow_fixed_glob {
1931
+ $filtered | where {|item|
1932
+ # Any-mode listings already include directory symlinks; keep them
1933
+ # navigable without changing their existing completion rendering.
1934
+ let navigation_dir = ($item.type == dir) or ($item.type == symlink and (try { ($item.name | path expand | path type) == "dir" } catch { false }))
1935
+ let name = ($env.PWD | path join $item.name | path expand --no-symlink)
1936
+ let normalized_name = if $is_win { $name | str downcase } else { $name }
1937
+ $navigation_dir or ($normalized_name in $fixed_matches)
1938
+ }
1939
+ } else { $filtered }
1940
+
1783
1941
  # Extract directory prefix to preserve in completion text
1784
1942
  let dir_prefix = if ($glob_base | is-empty) {
1785
1943
  ""
@@ -2055,6 +2213,9 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
2055
2213
  \$prefixFromInput = \$valuePrefix -and \$prefix -ceq \$valuePrefix
2056
2214
  \$lookupPrefix = if (\$prefixFromInput -and \$null -ne \$quoteType -and \$prefix.StartsWith('~')) { './' + \$prefix } else { \$prefix }
2057
2215
 
2216
+ # Detect narrowing before directory normalization changes prefix.
2217
+ \$narrowFixedGlob = \$type -ne 'directory' -and \$pattern -match '[\*\?]' -and \$prefix -ceq \$valuePrefix -and \$pattern -cne \$valuePrefix
2218
+
2058
2219
  # If prefix is a directory without trailing slash, append
2059
2220
  # one so Get-ChildItem lists its contents
2060
2221
  if (\$prefix -and (Test-Path -Path \$lookupPrefix -PathType Container) -and -not \$prefix.EndsWith('/') -and -not \$prefix.EndsWith('\\')) {
@@ -2066,6 +2227,16 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
2066
2227
  # characters, use it as-is; otherwise append *
2067
2228
  \$globPath = if (\$lookupPrefix -match '[\*\?]') { \$lookupPrefix } else { "\${lookupPrefix}*" }
2068
2229
 
2230
+ # Keep the original native glob as an independent file filter
2231
+ # when the current word narrows its candidates.
2232
+ \$fixedMatches = @()
2233
+ if (\$narrowFixedGlob) {
2234
+ # Include hidden parents here; candidate enumeration still
2235
+ # controls which hidden children can be suggested.
2236
+ \$fixedMatches = @(Get-Item -Force -Path \$pattern -ErrorAction SilentlyContinue |
2237
+ Select-Object -ExpandProperty FullName)
2238
+ }
2239
+
2069
2240
  # Get file system items based on type
2070
2241
  \$items = @()
2071
2242
  switch (\$type) {
@@ -2118,6 +2289,14 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
2118
2289
  \$items = \$items | Where-Object { -not \$_.Attributes.HasFlag([System.IO.FileAttributes]::Hidden) }
2119
2290
  }
2120
2291
 
2292
+ if (\$narrowFixedGlob) {
2293
+ \$items = \$items | Where-Object {
2294
+ if (\$_.PSIsContainer) { \$true }
2295
+ elseif (\$IsWindows) { \$fixedMatches -contains \$_.FullName }
2296
+ else { \$fixedMatches -ccontains \$_.FullName }
2297
+ }
2298
+ }
2299
+
2121
2300
  # Extract directory prefix to preserve in completion text
2122
2301
  \$dirPrefix = if (\$prefix -and (\$prefix.Contains('/') -or \$prefix.Contains('\\'))) {
2123
2302
  \$slashIdx = [Math]::Max(\$prefix.LastIndexOf('/'), \$prefix.LastIndexOf('\\'))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optique/core",
3
- "version": "1.0.15-dev.2873",
3
+ "version": "1.0.15-dev.2883",
4
4
  "description": "Type-safe combinatorial command-line interface parser",
5
5
  "keywords": [
6
6
  "CLI",