@optique/core 1.0.15-dev.2880 → 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]"
@@ -861,6 +862,9 @@ function ${functionName} () {
861
862
  unsetopt sh_glob
862
863
  local __was_extended_glob=0
863
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
864
868
 
865
869
  # Enable glob_dots when hidden files are requested so that
866
870
  # _files and _directories include dot-prefixed entries
@@ -873,6 +877,8 @@ function ${functionName} () {
873
877
  # current word. If the user has already typed beyond the pattern,
874
878
  # keep PREFIX unchanged for incremental narrowing.
875
879
  local __saved_prefix="\$PREFIX"
880
+ local __fixed_glob=0
881
+ local -a __fixed_matches
876
882
  local __initial_file_pattern="" __initial_directory="" __initial_leaf=""
877
883
  if [[ ( "\$type" == "file" || "\$type" == "any" || "\$type" == "directory" ) &&
878
884
  -z "\$current\$PREFIX\$SUFFIX" &&
@@ -910,8 +916,70 @@ function ${functionName} () {
910
916
  [[ "\$__compare_pattern" == */* ]] && __compare_pattern="\${__compare_pattern%/*}/" || __compare_pattern=""
911
917
  fi
912
918
  if [[ ( -n "\$__compare_pattern" || -n "\$__norm_prefix" ) && \${#__norm_prefix} -ge \${#__compare_pattern} && "\${__norm_prefix[1,\${#__compare_pattern}]}" == "\$__compare_pattern" && "\$PREFIX" != "\$pattern" ]]; then
913
- # User typed an equivalent or extended form — keep PREFIX
914
- 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
915
983
  else
916
984
  PREFIX="\$pattern"
917
985
  fi
@@ -935,6 +1003,9 @@ function ${functionName} () {
935
1003
  # Regular-file suggestions retain symlinked files; initial
936
1004
  # any globs use a non-directory qualifier instead.
937
1005
  local file_pattern="\${ext_pattern:-*}(#q-.)"
1006
+ if [[ "\$__fixed_glob" == "1" ]]; then
1007
+ file_pattern="\${ext_pattern:-*}(#q-.e:${fixedGlobFunction}:)"
1008
+ fi
938
1009
  if [[ -n "\$__initial_file_pattern" ]]; then
939
1010
  local __file_qualifier="-."
940
1011
  [[ "\$type" == "any" ]] && __file_qualifier="^-/"
@@ -971,7 +1042,7 @@ function ${functionName} () {
971
1042
  local -a file_ignore=() file_ignore_args=()
972
1043
  # Explicit globs bypass automatic fignore. Apply it only
973
1044
  # when the tag has not supplied its own ignore patterns.
974
- if [[ -z "\$__initial_file_pattern" && -z "\$ext_pattern" && -z "\$_comp_no_ignore" && -n "\$FIGNORE" ]] && (( \${#_comp_ignore[@]} == 0 )); then
1045
+ if [[ -z "\$__initial_file_pattern" && -z "\$ext_pattern" && ( "\$__fixed_glob" != "1" || "\$pattern" == "*" ) && -z "\$_comp_no_ignore" && -n "\$FIGNORE" ]] && (( \${#_comp_ignore[@]} == 0 )); then
975
1046
  file_ignore=("?*\${^fignore[@]}")
976
1047
  file_ignore_args=(-F file_ignore)
977
1048
  fi
@@ -1001,8 +1072,12 @@ function ${functionName} () {
1001
1072
  done
1002
1073
  (( file_matched )) && break
1003
1074
  done
1004
- elif [[ -n "\$ext_pattern" ]]; then
1075
+ elif [[ -n "\$ext_pattern" || "\$__fixed_glob" == "1" ]]; then
1005
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
1006
1081
  _wanted files expl file _path_files -g "\${file_pattern}"
1007
1082
  _wanted directories expl directory _path_files -/
1008
1083
  else
@@ -1022,6 +1097,7 @@ function ${functionName} () {
1022
1097
  PREFIX="\$__saved_prefix"
1023
1098
  if [[ "\$__was_extended_glob" == "1" ]]; then setopt extended_glob; else unsetopt extended_glob; fi
1024
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
1025
1101
  if [[ "\$__was_glob_dots" == "1" ]]; then setopt glob_dots; else unsetopt glob_dots; fi
1026
1102
  else
1027
1103
  # Regular literal completion
@@ -1052,6 +1128,11 @@ function ${functionName} () {
1052
1128
  fi
1053
1129
  }
1054
1130
 
1131
+ function ${fixedGlobFunction} () {
1132
+ local __fixed_candidate="\${REPLY:a}"
1133
+ (( \${__fixed_matches[(Ie)\$__fixed_candidate]} ))
1134
+ }
1135
+
1055
1136
  if (( $+functions[compdef] )); then
1056
1137
  compdef ${functionName} ${programName}
1057
1138
  fi
@@ -1685,12 +1766,12 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
1685
1766
  # prefix for incremental narrowing. Normalize path separators
1686
1767
  # before comparing so that Windows backslashes match forward slashes
1687
1768
  # in the transported pattern.
1769
+ let is_win = (($nu.os-info.name | str downcase) == "windows")
1688
1770
  let glob_base = if ($pattern | is-not-empty) {
1689
1771
  # Normalize separators and leading ./; downcase only on Windows
1690
1772
  # where filesystems are typically case-insensitive
1691
1773
  let norm_prefix_raw = ($prefix | str replace -a '\\' '/' | str replace -r '^\\./' '')
1692
1774
  let norm_pattern_raw = ($pattern | str replace -a '\\' '/' | str replace -r '^\\./' '')
1693
- let is_win = (($nu.os-info.name | str downcase) == "windows")
1694
1775
  let norm_prefix = (if $is_win { $norm_prefix_raw | str downcase } else { $norm_prefix_raw })
1695
1776
  let norm_pattern = (if $is_win { $norm_pattern_raw | str downcase } else { $norm_pattern_raw })
1696
1777
  # For wildcard patterns, compare only the directory prefix
@@ -1709,6 +1790,21 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
1709
1790
  $prefix
1710
1791
  }
1711
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
+
1712
1808
  # If the glob base is a directory without a trailing slash (e.g., "src"),
1713
1809
  # append one so that ls enumerates its contents instead of matching
1714
1810
  # siblings like "src-old". path expand follows symlinks, so that
@@ -1831,6 +1927,17 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
1831
1927
  }
1832
1928
  }
1833
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
+
1834
1941
  # Extract directory prefix to preserve in completion text
1835
1942
  let dir_prefix = if ($glob_base | is-empty) {
1836
1943
  ""
@@ -2106,6 +2213,9 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
2106
2213
  \$prefixFromInput = \$valuePrefix -and \$prefix -ceq \$valuePrefix
2107
2214
  \$lookupPrefix = if (\$prefixFromInput -and \$null -ne \$quoteType -and \$prefix.StartsWith('~')) { './' + \$prefix } else { \$prefix }
2108
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
+
2109
2219
  # If prefix is a directory without trailing slash, append
2110
2220
  # one so Get-ChildItem lists its contents
2111
2221
  if (\$prefix -and (Test-Path -Path \$lookupPrefix -PathType Container) -and -not \$prefix.EndsWith('/') -and -not \$prefix.EndsWith('\\')) {
@@ -2117,6 +2227,16 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
2117
2227
  # characters, use it as-is; otherwise append *
2118
2228
  \$globPath = if (\$lookupPrefix -match '[\*\?]') { \$lookupPrefix } else { "\${lookupPrefix}*" }
2119
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
+
2120
2240
  # Get file system items based on type
2121
2241
  \$items = @()
2122
2242
  switch (\$type) {
@@ -2169,6 +2289,14 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
2169
2289
  \$items = \$items | Where-Object { -not \$_.Attributes.HasFlag([System.IO.FileAttributes]::Hidden) }
2170
2290
  }
2171
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
+
2172
2300
  # Extract directory prefix to preserve in completion text
2173
2301
  \$dirPrefix = if (\$prefix -and (\$prefix.Contains('/') -or \$prefix.Contains('\\'))) {
2174
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]"
@@ -861,6 +862,9 @@ function ${functionName} () {
861
862
  unsetopt sh_glob
862
863
  local __was_extended_glob=0
863
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
864
868
 
865
869
  # Enable glob_dots when hidden files are requested so that
866
870
  # _files and _directories include dot-prefixed entries
@@ -873,6 +877,8 @@ function ${functionName} () {
873
877
  # current word. If the user has already typed beyond the pattern,
874
878
  # keep PREFIX unchanged for incremental narrowing.
875
879
  local __saved_prefix="\$PREFIX"
880
+ local __fixed_glob=0
881
+ local -a __fixed_matches
876
882
  local __initial_file_pattern="" __initial_directory="" __initial_leaf=""
877
883
  if [[ ( "\$type" == "file" || "\$type" == "any" || "\$type" == "directory" ) &&
878
884
  -z "\$current\$PREFIX\$SUFFIX" &&
@@ -910,8 +916,70 @@ function ${functionName} () {
910
916
  [[ "\$__compare_pattern" == */* ]] && __compare_pattern="\${__compare_pattern%/*}/" || __compare_pattern=""
911
917
  fi
912
918
  if [[ ( -n "\$__compare_pattern" || -n "\$__norm_prefix" ) && \${#__norm_prefix} -ge \${#__compare_pattern} && "\${__norm_prefix[1,\${#__compare_pattern}]}" == "\$__compare_pattern" && "\$PREFIX" != "\$pattern" ]]; then
913
- # User typed an equivalent or extended form — keep PREFIX
914
- 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
915
983
  else
916
984
  PREFIX="\$pattern"
917
985
  fi
@@ -935,6 +1003,9 @@ function ${functionName} () {
935
1003
  # Regular-file suggestions retain symlinked files; initial
936
1004
  # any globs use a non-directory qualifier instead.
937
1005
  local file_pattern="\${ext_pattern:-*}(#q-.)"
1006
+ if [[ "\$__fixed_glob" == "1" ]]; then
1007
+ file_pattern="\${ext_pattern:-*}(#q-.e:${fixedGlobFunction}:)"
1008
+ fi
938
1009
  if [[ -n "\$__initial_file_pattern" ]]; then
939
1010
  local __file_qualifier="-."
940
1011
  [[ "\$type" == "any" ]] && __file_qualifier="^-/"
@@ -971,7 +1042,7 @@ function ${functionName} () {
971
1042
  local -a file_ignore=() file_ignore_args=()
972
1043
  # Explicit globs bypass automatic fignore. Apply it only
973
1044
  # when the tag has not supplied its own ignore patterns.
974
- if [[ -z "\$__initial_file_pattern" && -z "\$ext_pattern" && -z "\$_comp_no_ignore" && -n "\$FIGNORE" ]] && (( \${#_comp_ignore[@]} == 0 )); then
1045
+ if [[ -z "\$__initial_file_pattern" && -z "\$ext_pattern" && ( "\$__fixed_glob" != "1" || "\$pattern" == "*" ) && -z "\$_comp_no_ignore" && -n "\$FIGNORE" ]] && (( \${#_comp_ignore[@]} == 0 )); then
975
1046
  file_ignore=("?*\${^fignore[@]}")
976
1047
  file_ignore_args=(-F file_ignore)
977
1048
  fi
@@ -1001,8 +1072,12 @@ function ${functionName} () {
1001
1072
  done
1002
1073
  (( file_matched )) && break
1003
1074
  done
1004
- elif [[ -n "\$ext_pattern" ]]; then
1075
+ elif [[ -n "\$ext_pattern" || "\$__fixed_glob" == "1" ]]; then
1005
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
1006
1081
  _wanted files expl file _path_files -g "\${file_pattern}"
1007
1082
  _wanted directories expl directory _path_files -/
1008
1083
  else
@@ -1022,6 +1097,7 @@ function ${functionName} () {
1022
1097
  PREFIX="\$__saved_prefix"
1023
1098
  if [[ "\$__was_extended_glob" == "1" ]]; then setopt extended_glob; else unsetopt extended_glob; fi
1024
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
1025
1101
  if [[ "\$__was_glob_dots" == "1" ]]; then setopt glob_dots; else unsetopt glob_dots; fi
1026
1102
  else
1027
1103
  # Regular literal completion
@@ -1052,6 +1128,11 @@ function ${functionName} () {
1052
1128
  fi
1053
1129
  }
1054
1130
 
1131
+ function ${fixedGlobFunction} () {
1132
+ local __fixed_candidate="\${REPLY:a}"
1133
+ (( \${__fixed_matches[(Ie)\$__fixed_candidate]} ))
1134
+ }
1135
+
1055
1136
  if (( $+functions[compdef] )); then
1056
1137
  compdef ${functionName} ${programName}
1057
1138
  fi
@@ -1685,12 +1766,12 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
1685
1766
  # prefix for incremental narrowing. Normalize path separators
1686
1767
  # before comparing so that Windows backslashes match forward slashes
1687
1768
  # in the transported pattern.
1769
+ let is_win = (($nu.os-info.name | str downcase) == "windows")
1688
1770
  let glob_base = if ($pattern | is-not-empty) {
1689
1771
  # Normalize separators and leading ./; downcase only on Windows
1690
1772
  # where filesystems are typically case-insensitive
1691
1773
  let norm_prefix_raw = ($prefix | str replace -a '\\' '/' | str replace -r '^\\./' '')
1692
1774
  let norm_pattern_raw = ($pattern | str replace -a '\\' '/' | str replace -r '^\\./' '')
1693
- let is_win = (($nu.os-info.name | str downcase) == "windows")
1694
1775
  let norm_prefix = (if $is_win { $norm_prefix_raw | str downcase } else { $norm_prefix_raw })
1695
1776
  let norm_pattern = (if $is_win { $norm_pattern_raw | str downcase } else { $norm_pattern_raw })
1696
1777
  # For wildcard patterns, compare only the directory prefix
@@ -1709,6 +1790,21 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
1709
1790
  $prefix
1710
1791
  }
1711
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
+
1712
1808
  # If the glob base is a directory without a trailing slash (e.g., "src"),
1713
1809
  # append one so that ls enumerates its contents instead of matching
1714
1810
  # siblings like "src-old". path expand follows symlinks, so that
@@ -1831,6 +1927,17 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
1831
1927
  }
1832
1928
  }
1833
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
+
1834
1941
  # Extract directory prefix to preserve in completion text
1835
1942
  let dir_prefix = if ($glob_base | is-empty) {
1836
1943
  ""
@@ -2106,6 +2213,9 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
2106
2213
  \$prefixFromInput = \$valuePrefix -and \$prefix -ceq \$valuePrefix
2107
2214
  \$lookupPrefix = if (\$prefixFromInput -and \$null -ne \$quoteType -and \$prefix.StartsWith('~')) { './' + \$prefix } else { \$prefix }
2108
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
+
2109
2219
  # If prefix is a directory without trailing slash, append
2110
2220
  # one so Get-ChildItem lists its contents
2111
2221
  if (\$prefix -and (Test-Path -Path \$lookupPrefix -PathType Container) -and -not \$prefix.EndsWith('/') -and -not \$prefix.EndsWith('\\')) {
@@ -2117,6 +2227,16 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
2117
2227
  # characters, use it as-is; otherwise append *
2118
2228
  \$globPath = if (\$lookupPrefix -match '[\*\?]') { \$lookupPrefix } else { "\${lookupPrefix}*" }
2119
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
+
2120
2240
  # Get file system items based on type
2121
2241
  \$items = @()
2122
2242
  switch (\$type) {
@@ -2169,6 +2289,14 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
2169
2289
  \$items = \$items | Where-Object { -not \$_.Attributes.HasFlag([System.IO.FileAttributes]::Hidden) }
2170
2290
  }
2171
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
+
2172
2300
  # Extract directory prefix to preserve in completion text
2173
2301
  \$dirPrefix = if (\$prefix -and (\$prefix.Contains('/') -or \$prefix.Contains('\\'))) {
2174
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.2880",
3
+ "version": "1.0.15-dev.2883",
4
4
  "description": "Type-safe combinatorial command-line interface parser",
5
5
  "keywords": [
6
6
  "CLI",