@optique/core 1.0.15-dev.2880 → 1.0.15-dev.2887

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,12 +805,27 @@ 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
- local current="\$words[CURRENT]"
811
+ # Completion fields omit surrounding quotes, unlike words[CURRENT].
812
+ # Single-quoted fields are already literal; other fields retain escapes.
813
+ local current="\$IPREFIX\$PREFIX\$SUFFIX\$ISUFFIX"
814
+ local __logical_prefix="\$IPREFIX\$PREFIX"
811
815
  # Let zsh match the suffix when completing in the middle of a word.
812
816
  if [[ -o complete_in_word ]]; then
813
- current="\$PREFIX"
817
+ current="\$__logical_prefix"
818
+ fi
819
+ if [[ "\$compstate[quote]" == '\"' ]]; then
820
+ # Restore the omitted quoting context before removing shell escapes.
821
+ # In double quotes, a backslash before an ordinary character is literal.
822
+ current="\$QIPREFIX\$current\${QISUFFIX:-\$compstate[quote]}"
823
+ __logical_prefix="\$QIPREFIX\$__logical_prefix\$compstate[quote]"
824
+ current="\${(Q)current}"
825
+ __logical_prefix="\${(Q)__logical_prefix}"
826
+ elif [[ "\$compstate[quote]" != "'" ]]; then
827
+ current="\${(Q)current}"
828
+ __logical_prefix="\${(Q)__logical_prefix}"
814
829
  fi
815
830
  local -a prev
816
831
  # Extract previous arguments, skipping empty ones
@@ -818,7 +833,7 @@ function ${functionName} () {
818
833
  local i
819
834
  for (( i=2; i < CURRENT; i++ )); do
820
835
  if [[ -n "\$words[i]" ]]; then
821
- prev+=("\$words[i]")
836
+ prev+=("\${(Q)words[i]}")
822
837
  fi
823
838
  done
824
839
 
@@ -861,6 +876,9 @@ function ${functionName} () {
861
876
  unsetopt sh_glob
862
877
  local __was_extended_glob=0
863
878
  [[ -o extended_glob ]] && __was_extended_glob=1
879
+ local __was_sh_word_split=0
880
+ [[ -o sh_word_split ]] && __was_sh_word_split=1
881
+ unsetopt sh_word_split
864
882
 
865
883
  # Enable glob_dots when hidden files are requested so that
866
884
  # _files and _directories include dot-prefixed entries
@@ -873,6 +891,8 @@ function ${functionName} () {
873
891
  # current word. If the user has already typed beyond the pattern,
874
892
  # keep PREFIX unchanged for incremental narrowing.
875
893
  local __saved_prefix="\$PREFIX"
894
+ local __fixed_glob=0
895
+ local -a __fixed_matches
876
896
  local __initial_file_pattern="" __initial_directory="" __initial_leaf=""
877
897
  if [[ ( "\$type" == "file" || "\$type" == "any" || "\$type" == "directory" ) &&
878
898
  -z "\$current\$PREFIX\$SUFFIX" &&
@@ -900,20 +920,322 @@ function ${functionName} () {
900
920
  fi
901
921
  fi
902
922
  fi
903
- if [[ -n "\$pattern" && -z "\$__initial_file_pattern" ]]; then
923
+ local __saved_pattern_match="\$compstate[pattern_match]"
924
+ local __pattern_match_changed=0
925
+ # path() echoes current as its pattern. Keep zsh's escaped PREFIX
926
+ # in that case instead of reinterpreting logical text as shell syntax.
927
+ if [[ -n "\$pattern" && "\$pattern" != "\$current" && -z "\$__initial_file_pattern" ]]; then
904
928
  # Normalize leading ./ so that ./src/ma matches pattern src/
905
- local __norm_prefix="\${PREFIX#./}"
929
+ local __norm_prefix="\${__logical_prefix#./}"
906
930
  local __norm_pattern="\${pattern#./}"
907
931
  local __compare_pattern="\$__norm_pattern"
908
932
  if [[ "\$__compare_pattern" == *[\*\?]* ]]; then
909
933
  __compare_pattern="\${__compare_pattern%%[\*\?]*}"
910
934
  [[ "\$__compare_pattern" == */* ]] && __compare_pattern="\${__compare_pattern%/*}/" || __compare_pattern=""
911
935
  fi
912
- 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
936
+ if [[ ( -n "\$__compare_pattern" || -n "\$__norm_prefix" ) && \${#__norm_prefix} -ge \${#__compare_pattern} && "\${__norm_prefix[1,\${#__compare_pattern}]}" == "\$__compare_pattern" && "\$__logical_prefix" != "\$pattern" ]]; then
937
+ # Keep the typed prefix, but retain a fixed file glob separately.
938
+ if [[ "\$type" != "directory" && "\$__norm_pattern" == *[\*\?]* ]]; then
939
+ __fixed_glob=1
940
+ local __fixed_was_glob_dots=0
941
+ [[ -o glob_dots ]] && __fixed_was_glob_dots=1
942
+ if [[ "\$PREFIX" == .* || "\$PREFIX" == */.* ]]; then setopt glob_dots; fi
943
+ # Keep qualifiers in a string so sourcing also works with SH_GLOB.
944
+ local __fixed_pattern="" __glob_char="" __glob_previous=""
945
+ local __glob_escaped=0
946
+ local __glob_class=0 __glob_class_start=0 __glob_class_nested=""
947
+ local -i __glob_index
948
+ for (( __glob_index=1; __glob_index <= \${#pattern}; __glob_index++ )); do
949
+ __glob_char="\${pattern[__glob_index]}"
950
+ if [[ "\$__glob_class" == "1" ]]; then
951
+ # Class members are already literal. Preserve leading ]
952
+ # and nested POSIX classes without adding backslashes.
953
+ __fixed_pattern+="\$__glob_char"
954
+ if [[ -n "\$__glob_class_nested" ]]; then
955
+ if [[ "\$__glob_char" == "]" && "\$__glob_previous" == "\$__glob_class_nested" ]]; then
956
+ __glob_class_nested=""
957
+ fi
958
+ elif [[ "\$__glob_char" == "[" && "\${pattern[__glob_index+1]}" == ":" ]]; then
959
+ __glob_class_nested="\${pattern[__glob_index+1]}"
960
+ __glob_class_start=0
961
+ elif [[ "\$__glob_char" == "]" && "\$__glob_class_start" == "0" ]]; then
962
+ __glob_class=0
963
+ elif [[ "\$__glob_class_start" == "1" && ( "\$__glob_char" == "!" || "\$__glob_char" == "^" ) &&
964
+ ( "\${pattern[__glob_index+1]}" != "]" || "\${pattern[__glob_index+2,-1]}" == *']'* ) ]]; then
965
+ __glob_class_start=2
966
+ else
967
+ __glob_class_start=0
968
+ fi
969
+ __glob_previous="\$__glob_char"
970
+ continue
971
+ fi
972
+ if [[ "\$__glob_escaped" == "1" ]]; then
973
+ __fixed_pattern+="\$__glob_char"
974
+ __glob_escaped=0
975
+ __glob_previous=""
976
+ continue
977
+ fi
978
+ if [[ "\$__glob_char" == "\\\\" ]]; then
979
+ __fixed_pattern+="\$__glob_char"
980
+ __glob_escaped=1
981
+ elif [[ "\$__glob_char" == "[" ]]; then
982
+ __fixed_pattern+="\$__glob_char"
983
+ __glob_class=1
984
+ __glob_class_start=1
985
+ elif [[ "\$__glob_char" == [#^~\\(\\)\\|\\<\\>] &&
986
+ ! ( "\$__glob_char" == "~" && "\$__glob_index" == "1" ) ]]; then
987
+ # Keep class negation and leading home expansion; escape
988
+ # extended-glob operators in literal pathname components.
989
+ __fixed_pattern+="\\\\\$__glob_char"
990
+ else
991
+ __fixed_pattern+="\$__glob_char"
992
+ fi
993
+ __glob_previous="\$__glob_char"
994
+ done
995
+ __fixed_pattern+="(#qN)"
996
+ __fixed_matches=( \${~__fixed_pattern} )
997
+ __fixed_matches=( "\${(@)__fixed_matches:a}" )
998
+ # Hidden parents widen fixed matches, not native child suggestions.
999
+ if [[ "\$__fixed_was_glob_dots" == "0" && "\${PREFIX##*/}" != .* ]]; then unsetopt glob_dots; fi
1000
+ fi
915
1001
  else
916
- PREFIX="\$pattern"
1002
+ local __native_pattern="\$pattern"
1003
+ # Initial globs use the conservative native filters above.
1004
+ # Retained input and protected markers still need pattern matching.
1005
+ if [[ -n "\$compstate[context]" && "\$pattern" == *[\\*\\?\\[]* &&
1006
+ ( -n "\$current" || "\$pattern" == *'$'* || "\$pattern" == *'\`'* ) ]]; then
1007
+ compstate[pattern_match]='*'
1008
+ __pattern_match_changed=1
1009
+ fi
1010
+ # Native helpers expand substitutions, tilde names and executable
1011
+ # glob qualifiers. Input-derived patterns must keep these literal.
1012
+ # Ordinary fixed glob/tilde patterns retain their native behavior.
1013
+ local __tilde_name="\${pattern#\\~}"
1014
+ __tilde_name="\${__tilde_name%%/*}"
1015
+ local -a match mbegin mend
1016
+ local __code_qualifier=0
1017
+ local __fixed_qualifier_base=""
1018
+ local __safe_qualifiers='([-/F.@=p*rwxAIERWXsStUG^MTNDn,]|[ugdl]<->|[amc]([Mwhmsd]|)([-+]|)<->|L([kKmMgGtTpP]|)([-+]|)<->|[oO][nLlamcdN])##'
1019
+ local __qualifier_pattern="\$pattern" __qualifier
1020
+ while _have_glob_qual "\$__qualifier_pattern" complete; do
1021
+ __qualifier="\${match[5]#\\#q}"
1022
+ __qualifier_pattern="\$match[2]"
1023
+ # A numeric sense (+1) is not a function qualifier (+name).
1024
+ # Accept only bounded attribute/sort syntax in this exception.
1025
+ # Expand the pattern at runtime after unsetting SH_GLOB.
1026
+ if [[ "\$__qualifier" == \${~__safe_qualifiers} ]]; then
1027
+ __fixed_qualifier_base="\$__qualifier_pattern"
1028
+ elif [[ "\$__qualifier" == *[e+]* ]]; then
1029
+ __code_qualifier=1
1030
+ fi
1031
+ done
1032
+ # Partial overlaps with complete backend classes/groups remain
1033
+ # syntax. A complete input-provided expression can be literal.
1034
+ local -a __syntax_starts=() __syntax_ends=() __group_stack=()
1035
+ local -i __scan_index __class_begin=0 __member_begin=0
1036
+ local __scan_char __nested_class=""
1037
+ for (( __scan_index=1; __scan_index <= \${#pattern}; __scan_index++ )); do
1038
+ __scan_char="\${pattern[__scan_index]}"
1039
+ if [[ "\$__scan_char" == '\\' ]]; then
1040
+ (( __scan_index++ ))
1041
+ elif (( __class_begin )); then
1042
+ if [[ "\$__scan_char" == '[' && "\${pattern[__scan_index+1]}" == [:.=] ]]; then
1043
+ __nested_class="\${pattern[__scan_index+1]}"
1044
+ elif [[ -n "\$__nested_class" && "\$__scan_char" == "\$__nested_class" && "\${pattern[__scan_index+1]}" == ']' ]]; then
1045
+ (( __scan_index++ ))
1046
+ __nested_class=""
1047
+ elif [[ -z "\$__nested_class" && "\$__scan_char" == ']' ]] && (( __scan_index > __member_begin )); then
1048
+ __syntax_starts+=("\$__class_begin")
1049
+ __syntax_ends+=("\$__scan_index")
1050
+ __class_begin=0
1051
+ elif (( __scan_index == __member_begin )) &&
1052
+ [[ "\$__scan_char" == '!' || "\$__scan_char" == '^' ]] &&
1053
+ [[ "\${pattern[__scan_index+1]}" != ']' || "\${pattern[__scan_index+2,-1]}" == *']'* ]]; then
1054
+ (( __member_begin++ ))
1055
+ fi
1056
+ elif [[ "\$__scan_char" == '[' ]]; then
1057
+ __class_begin=\$__scan_index
1058
+ __member_begin=\$(( __scan_index + 1 ))
1059
+ elif [[ "\$__scan_char" == '(' ]]; then
1060
+ __group_stack+=("\$__scan_index")
1061
+ elif [[ "\$__scan_char" == ')' ]] && (( \${#__group_stack} )); then
1062
+ __syntax_starts+=("\${__group_stack[-1]}")
1063
+ __syntax_ends+=("\$__scan_index")
1064
+ __group_stack[-1]=()
1065
+ fi
1066
+ done
1067
+ # Search retained input only where it cannot split fixed syntax.
1068
+ local __input_part="\$current"
1069
+ local __backend_prefix="\$pattern" __backend_suffix=""
1070
+ local __search_pattern __candidate_prefix
1071
+ local -i __search_offset __overlap_start __overlap_end __range_index __syntax_overlap
1072
+ local -i __fixed_tilde_end=0
1073
+ local __tilde_expression='([+-]|[+-]<->|[[:alnum:]_.-]#)'
1074
+ if [[ "\$pattern" == '~'* &&
1075
+ "\$__tilde_name" == \${~__tilde_expression} ]]; then
1076
+ __fixed_tilde_end=\$(( \${#__tilde_name} + 1 ))
1077
+ fi
1078
+ while [[ -n "\$__input_part" ]]; do
1079
+ __search_pattern="\$pattern"
1080
+ __search_offset=0
1081
+ while [[ "\$__search_pattern" == *"\$__input_part"* ]]; do
1082
+ __candidate_prefix="\${__search_pattern%%"\$__input_part"*}"
1083
+ __overlap_start=\$(( __search_offset + \${#__candidate_prefix} + 1 ))
1084
+ __overlap_end=\$(( __overlap_start + \${#__input_part} - 1 ))
1085
+ __syntax_overlap=0
1086
+ # Leading home/directory-stack/named-directory syntax belongs
1087
+ # to the fixed pattern, even when the input overlaps it.
1088
+ if (( __fixed_tilde_end && __overlap_start <= __fixed_tilde_end )); then
1089
+ __syntax_overlap=1
1090
+ fi
1091
+ for (( __range_index=1; __range_index <= \${#__syntax_starts}; __range_index++ )); do
1092
+ if (( __overlap_start <= __syntax_ends[__range_index] &&
1093
+ __overlap_end >= __syntax_starts[__range_index] &&
1094
+ ( __overlap_start > __syntax_starts[__range_index] ||
1095
+ __overlap_end < __syntax_ends[__range_index] ) )); then
1096
+ __syntax_overlap=1
1097
+ break
1098
+ fi
1099
+ done
1100
+ # Every position in a recognized fixed attribute qualifier
1101
+ # is backend syntax, including its closing parenthesis.
1102
+ if [[ "\$__fixed_qualifier_base" == *[\\*\\?\\]] ]] &&
1103
+ (( __overlap_end > \${#__fixed_qualifier_base} )); then
1104
+ __syntax_overlap=1
1105
+ fi
1106
+ if (( ! __syntax_overlap )); then
1107
+ if (( __overlap_start == 1 )); then
1108
+ __backend_prefix=""
1109
+ else
1110
+ __backend_prefix="\${pattern[1,__overlap_start-1]}"
1111
+ fi
1112
+ __backend_suffix="\${pattern[__overlap_end+1,-1]}"
1113
+ break 2
1114
+ fi
1115
+ __search_offset=\$__overlap_start
1116
+ __search_pattern="\${pattern[__overlap_start+1,-1]}"
1117
+ done
1118
+ __input_part="\${__input_part[2,-1]}"
1119
+ done
1120
+ local __quoted_input="\${(q)__input_part}"
1121
+ # Pattern matching removes quoting differently from normal
1122
+ # completion. Character classes keep these characters literal.
1123
+ if [[ -n "\$compstate[pattern_match]" ]]; then
1124
+ __quoted_input="\${(b)__input_part}"
1125
+ __quoted_input="\${__quoted_input//\\\$/[\\\$]}"
1126
+ __quoted_input="\${__quoted_input//\\\`/[\\\`]}"
1127
+ fi
1128
+ if [[ "\$pattern" == *'$'* || "\$pattern" == *'\`'* ||
1129
+ ( "\$pattern" == '~'* && "\$__tilde_name" == *[^[:alnum:]_.+-]* ) ]] ||
1130
+ (( __code_qualifier )); then
1131
+ __native_pattern="\${(q)pattern}"
1132
+ local __backend_pattern="\$__backend_prefix\$__backend_suffix"
1133
+ local __backend_tilde="\${__backend_pattern#\\~}"
1134
+ __backend_tilde="\${__backend_tilde%%/*}"
1135
+ local __unsafe_backend_qualifier=0
1136
+ __qualifier_pattern="\$__backend_pattern"
1137
+ while _have_glob_qual "\$__qualifier_pattern" complete; do
1138
+ __qualifier="\${match[5]#\\#q}"
1139
+ __qualifier_pattern="\$match[2]"
1140
+ if [[ "\$__qualifier" != \${~__safe_qualifiers} ]]; then
1141
+ __unsafe_backend_qualifier=1
1142
+ fi
1143
+ done
1144
+ # Retain trusted wildcards and safe attribute qualifiers when
1145
+ # all executable syntax belongs to the quoted input.
1146
+ if [[ -n "\$__input_part" &&
1147
+ "\$__backend_pattern" != *'$'* &&
1148
+ "\$__backend_pattern" != *'\`'* &&
1149
+ ( "\$__backend_pattern" != '~'* ||
1150
+ "\$__backend_tilde" != *[^[:alnum:]_.+-]* ) ]] &&
1151
+ (( ! __unsafe_backend_qualifier )); then
1152
+ __native_pattern="\$__backend_prefix\$__quoted_input\$__backend_suffix"
1153
+ elif [[ "\$__backend_pattern" != *'$('* &&
1154
+ "\$__backend_pattern" != *'\${'* &&
1155
+ "\$__backend_pattern" != *'$['* &&
1156
+ ( "\$pattern" != '~'* ||
1157
+ "\$__tilde_name" != *[^[:alnum:]_.+-]* ) ]] &&
1158
+ (( ! __unsafe_backend_qualifier )); then
1159
+ # Fixed patterns also need literal substitution markers,
1160
+ # without quoting their wildcards or safe qualifiers.
1161
+ local __dollar='\\\$'
1162
+ local __backtick='\\\`'
1163
+ if [[ -n "\$compstate[pattern_match]" ]]; then
1164
+ __dollar='[\$]'
1165
+ __backtick='[\`]'
1166
+ fi
1167
+ # Existing glob classes already protect their markers.
1168
+ local -a __backend_parts=()
1169
+ local __part __escaped __char __class_kind
1170
+ local -i __index __in_class __class_start
1171
+ for __part in "\$__backend_prefix" "\$__backend_suffix"; do
1172
+ __escaped=""
1173
+ __in_class=0
1174
+ __class_kind=""
1175
+ for (( __index=1; __index <= \${#__part}; __index++ )); do
1176
+ __char="\${__part[__index]}"
1177
+ if [[ "\$__char" == '\\' ]]; then
1178
+ __escaped+="\$__char\${__part[__index+1]}"
1179
+ (( __index++ ))
1180
+ continue
1181
+ elif (( __in_class )); then
1182
+ if [[ "\$__char" == '[' && "\${__part[__index+1]}" == [:.=] ]]; then
1183
+ __class_kind="\${__part[__index+1]}"
1184
+ elif [[ -n "\$__class_kind" && "\$__char" == "\$__class_kind" && "\${__part[__index+1]}" == ']' ]]; then
1185
+ __escaped+="\$__char]"
1186
+ (( __index++ ))
1187
+ __class_kind=""
1188
+ continue
1189
+ elif [[ -z "\$__class_kind" && "\$__char" == ']' ]] && (( __index > __class_start )); then
1190
+ __in_class=0
1191
+ elif (( __index == __class_start )) && [[ "\$__char" == '!' || "\$__char" == '^' ]]; then
1192
+ (( __class_start++ ))
1193
+ fi
1194
+ elif [[ "\$__char" == '[' ]]; then
1195
+ __in_class=1
1196
+ __class_start=$(( __index + 1 ))
1197
+ elif [[ "\$__char" == '$' ]]; then
1198
+ __char="\$__dollar"
1199
+ elif [[ "\$__char" == '\`' ]]; then
1200
+ __char="\$__backtick"
1201
+ fi
1202
+ __escaped+="\$__char"
1203
+ done
1204
+ __backend_parts+=("\$__escaped")
1205
+ done
1206
+ __backend_prefix="\${__backend_parts[1]}"
1207
+ __backend_suffix="\${__backend_parts[2]}"
1208
+ __native_pattern="\$__backend_prefix\$__quoted_input\$__backend_suffix"
1209
+ fi
1210
+ else
1211
+ # Quote retained metacharacters, including segments made only
1212
+ # of them. Wildcard-only overlap remains backend syntax.
1213
+ if [[ "\$__input_part" == *[\\\\\\(\\)\\[\\#\\^\\|\\~\\<\\>\\*\\?]* &&
1214
+ "\$__input_part" != [\\*\\?]## ]]; then
1215
+ __native_pattern="\$__backend_prefix\$__quoted_input\$__backend_suffix"
1216
+ fi
1217
+ fi
1218
+ PREFIX="\$__native_pattern"
1219
+ # Match simple fixed tilde globs through native -g filters. The
1220
+ # pattern matcher otherwise treats the leading ~ as glob syntax,
1221
+ # even after _path_files has expanded it for filesystem lookup.
1222
+ if (( __fixed_tilde_end )) && [[ -z "\$__input_part" &&
1223
+ "\$__native_pattern" == "\$pattern" && "\$pattern" == */* ]]; then
1224
+ __initial_directory="\${pattern%/*}/"
1225
+ __initial_leaf="\${pattern##*/}"
1226
+ if [[ "\$__initial_leaf" == *[\*\?]* &&
1227
+ "\$__initial_directory" != *[\*\?]* &&
1228
+ "\$__initial_directory" != *["[]()"]* &&
1229
+ "\$__initial_leaf" != *["[]()#~^|<>;&"]* &&
1230
+ "\$__initial_leaf" != *'\\'* &&
1231
+ "\$__initial_leaf" != *[[:space:]]* &&
1232
+ "\$__initial_leaf" != *"'"* && "\$__initial_leaf" != *'"'* ]]; then
1233
+ __initial_file_pattern="\$__initial_leaf"
1234
+ PREFIX="\$__initial_directory"
1235
+ compstate[pattern_match]=""
1236
+ __pattern_match_changed=1
1237
+ fi
1238
+ fi
917
1239
  fi
918
1240
  fi
919
1241
 
@@ -935,6 +1257,9 @@ function ${functionName} () {
935
1257
  # Regular-file suggestions retain symlinked files; initial
936
1258
  # any globs use a non-directory qualifier instead.
937
1259
  local file_pattern="\${ext_pattern:-*}(#q-.)"
1260
+ if [[ "\$__fixed_glob" == "1" ]]; then
1261
+ file_pattern="\${ext_pattern:-*}(#q-.e:${fixedGlobFunction}:)"
1262
+ fi
938
1263
  if [[ -n "\$__initial_file_pattern" ]]; then
939
1264
  local __file_qualifier="-."
940
1265
  [[ "\$type" == "any" ]] && __file_qualifier="^-/"
@@ -950,6 +1275,21 @@ function ${functionName} () {
950
1275
  fi
951
1276
  file_pattern="\${(j: :)__initial_file_patterns}"
952
1277
  fi
1278
+ local file_pattern_prefix="\$PREFIX"
1279
+ local -a match mbegin mend
1280
+ # Native qualifier copying concatenates flags with the explicit
1281
+ # regular-file group. Keep backend attributes in their own group.
1282
+ if [[ -n "\$compstate[pattern_match]" && -z "\$SUFFIX" ]]; then
1283
+ local file_qualifier file_qualifiers=""
1284
+ while _have_glob_qual "\$file_pattern_prefix" complete; do
1285
+ file_qualifier="\${match[5]#\\#q}"
1286
+ file_pattern_prefix="\$match[2]"
1287
+ file_qualifiers="(#q\$file_qualifier)\$file_qualifiers"
1288
+ done
1289
+ if [[ -n "\$file_qualifiers" ]]; then
1290
+ file_pattern+="\$file_qualifiers"
1291
+ fi
1292
+ fi
953
1293
  local file_subtree_pattern="**/*(#q/N)"
954
1294
  local -a file_subtree=() file_recursive_patterns=()
955
1295
  local -a file_tags=(files directories)
@@ -963,15 +1303,22 @@ function ${functionName} () {
963
1303
  local file_matched=0
964
1304
  for file_tag in "\${file_tags[@]}"; do
965
1305
  if [[ "\$file_tag" == directories ]]; then
1306
+ local file_tag_prefix="\$PREFIX"
1307
+ PREFIX="\$file_pattern_prefix"
966
1308
  while _next_label directories expl directory; do
1309
+ # File qualifiers constrain final results, not the
1310
+ # directories needed to navigate to those results.
967
1311
  _path_files -/ "\${expl[@]}" && file_matched=1
968
1312
  done
1313
+ PREFIX="\$file_tag_prefix"
969
1314
  else
1315
+ local file_tag_prefix="\$PREFIX"
1316
+ PREFIX="\$file_pattern_prefix"
970
1317
  while _next_label files expl file; do
971
1318
  local -a file_ignore=() file_ignore_args=()
972
1319
  # Explicit globs bypass automatic fignore. Apply it only
973
1320
  # 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
1321
+ if [[ -z "\$__initial_file_pattern" && -z "\$ext_pattern" && ( "\$__fixed_glob" != "1" || "\$pattern" == "*" ) && -z "\$_comp_no_ignore" && -n "\$FIGNORE" ]] && (( \${#_comp_ignore[@]} == 0 )); then
975
1322
  file_ignore=("?*\${^fignore[@]}")
976
1323
  file_ignore_args=(-F file_ignore)
977
1324
  fi
@@ -997,12 +1344,17 @@ function ${functionName} () {
997
1344
  done
998
1345
  fi
999
1346
  done
1347
+ PREFIX="\$file_tag_prefix"
1000
1348
  fi
1001
1349
  done
1002
1350
  (( file_matched )) && break
1003
1351
  done
1004
- elif [[ -n "\$ext_pattern" ]]; then
1352
+ elif [[ -n "\$ext_pattern" || "\$__fixed_glob" == "1" ]]; then
1005
1353
  local file_pattern="\${ext_pattern}(#q^-/)"
1354
+ if [[ "\$__fixed_glob" == "1" ]]; then
1355
+ # Put the predicate before ^, which negates later qualifiers.
1356
+ file_pattern="\${ext_pattern:-*}(#qe:${fixedGlobFunction}:^-/)"
1357
+ fi
1006
1358
  _wanted files expl file _path_files -g "\${file_pattern}"
1007
1359
  _wanted directories expl directory _path_files -/
1008
1360
  else
@@ -1021,7 +1373,11 @@ function ${functionName} () {
1021
1373
  # Restore PREFIX and glob_dots to their previous state
1022
1374
  PREFIX="\$__saved_prefix"
1023
1375
  if [[ "\$__was_extended_glob" == "1" ]]; then setopt extended_glob; else unsetopt extended_glob; fi
1376
+ if (( __pattern_match_changed )); then
1377
+ compstate[pattern_match]="\$__saved_pattern_match"
1378
+ fi
1024
1379
  if [[ "\$__was_sh_glob" == "1" ]]; then setopt sh_glob; else unsetopt sh_glob; fi
1380
+ if [[ "\$__was_sh_word_split" == "1" ]]; then setopt sh_word_split; else unsetopt sh_word_split; fi
1025
1381
  if [[ "\$__was_glob_dots" == "1" ]]; then setopt glob_dots; else unsetopt glob_dots; fi
1026
1382
  else
1027
1383
  # Regular literal completion
@@ -1038,20 +1394,31 @@ function ${functionName} () {
1038
1394
  # Prepare completion with descriptions for _describe
1039
1395
  local -a matches
1040
1396
  local -i i
1397
+ local __match
1041
1398
  for (( i=1; i <= \${#completions[@]}; i++ )); do
1042
- # _describe treats colons as separators and backslashes as escapes.
1043
- value="\${completions[i]//\\\\/\\\\\\\\}"
1044
- value="\${value//:/\\\\:}"
1399
+ # _describe uses backslashes to escape its colon separator.
1400
+ __match="\${completions[i]//\\\\/\\\\\\\\}"
1401
+ __match="\${__match//:/\\\\:}"
1045
1402
  if [[ -n "\${descriptions[i]}" ]]; then
1046
- matches+=("\${value}:\${descriptions[i]}")
1403
+ matches+=("\$__match:\${descriptions[i]}")
1047
1404
  else
1048
- matches+=("\${value}")
1405
+ matches+=("\$__match")
1049
1406
  fi
1050
1407
  done
1051
- _describe 'commands' matches
1408
+ if [[ -n "\$QISUFFIX" ]]; then
1409
+ # A closing quote already exists; do not append a space inside it.
1410
+ _describe 'commands' matches -S ''
1411
+ else
1412
+ _describe 'commands' matches
1413
+ fi
1052
1414
  fi
1053
1415
  }
1054
1416
 
1417
+ function ${fixedGlobFunction} () {
1418
+ local __fixed_candidate="\${REPLY:a}"
1419
+ (( \${__fixed_matches[(Ie)\$__fixed_candidate]} ))
1420
+ }
1421
+
1055
1422
  if (( $+functions[compdef] )); then
1056
1423
  compdef ${functionName} ${programName}
1057
1424
  fi
@@ -1685,12 +2052,12 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
1685
2052
  # prefix for incremental narrowing. Normalize path separators
1686
2053
  # before comparing so that Windows backslashes match forward slashes
1687
2054
  # in the transported pattern.
2055
+ let is_win = (($nu.os-info.name | str downcase) == "windows")
1688
2056
  let glob_base = if ($pattern | is-not-empty) {
1689
2057
  # Normalize separators and leading ./; downcase only on Windows
1690
2058
  # where filesystems are typically case-insensitive
1691
2059
  let norm_prefix_raw = ($prefix | str replace -a '\\' '/' | str replace -r '^\\./' '')
1692
2060
  let norm_pattern_raw = ($pattern | str replace -a '\\' '/' | str replace -r '^\\./' '')
1693
- let is_win = (($nu.os-info.name | str downcase) == "windows")
1694
2061
  let norm_prefix = (if $is_win { $norm_prefix_raw | str downcase } else { $norm_prefix_raw })
1695
2062
  let norm_pattern = (if $is_win { $norm_pattern_raw | str downcase } else { $norm_pattern_raw })
1696
2063
  # For wildcard patterns, compare only the directory prefix
@@ -1709,6 +2076,21 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
1709
2076
  $prefix
1710
2077
  }
1711
2078
 
2079
+ # Intersect fixed glob matches with the narrowed prefix candidates.
2080
+ # Directory-only suggestions retain their existing navigation behavior.
2081
+ let narrow_fixed_glob = ($type != "directory") and ($pattern =~ '[*?]') and ($prefix != $pattern) and ($glob_base == $prefix)
2082
+ let fixed_matches = if $narrow_fixed_glob {
2083
+ try {
2084
+ let fixed_pattern = ($pattern | into glob)
2085
+ let include_hidden = $hidden or ($prefix | path basename | str starts-with '.')
2086
+ let matches = if $include_hidden { ls -a --directory $fixed_pattern } else { ls --directory $fixed_pattern }
2087
+ $matches | each {|item|
2088
+ let name = ($env.PWD | path join $item.name | path expand --no-symlink)
2089
+ if $is_win { $name | str downcase } else { $name }
2090
+ }
2091
+ } catch { [] }
2092
+ } else { [] }
2093
+
1712
2094
  # If the glob base is a directory without a trailing slash (e.g., "src"),
1713
2095
  # append one so that ls enumerates its contents instead of matching
1714
2096
  # siblings like "src-old". path expand follows symlinks, so that
@@ -1831,6 +2213,17 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
1831
2213
  }
1832
2214
  }
1833
2215
 
2216
+ let filtered = if $narrow_fixed_glob {
2217
+ $filtered | where {|item|
2218
+ # Any-mode listings already include directory symlinks; keep them
2219
+ # navigable without changing their existing completion rendering.
2220
+ let navigation_dir = ($item.type == dir) or ($item.type == symlink and (try { ($item.name | path expand | path type) == "dir" } catch { false }))
2221
+ let name = ($env.PWD | path join $item.name | path expand --no-symlink)
2222
+ let normalized_name = if $is_win { $name | str downcase } else { $name }
2223
+ $navigation_dir or ($normalized_name in $fixed_matches)
2224
+ }
2225
+ } else { $filtered }
2226
+
1834
2227
  # Extract directory prefix to preserve in completion text
1835
2228
  let dir_prefix = if ($glob_base | is-empty) {
1836
2229
  ""
@@ -2106,6 +2499,9 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
2106
2499
  \$prefixFromInput = \$valuePrefix -and \$prefix -ceq \$valuePrefix
2107
2500
  \$lookupPrefix = if (\$prefixFromInput -and \$null -ne \$quoteType -and \$prefix.StartsWith('~')) { './' + \$prefix } else { \$prefix }
2108
2501
 
2502
+ # Detect narrowing before directory normalization changes prefix.
2503
+ \$narrowFixedGlob = \$type -ne 'directory' -and \$pattern -match '[\*\?]' -and \$prefix -ceq \$valuePrefix -and \$pattern -cne \$valuePrefix
2504
+
2109
2505
  # If prefix is a directory without trailing slash, append
2110
2506
  # one so Get-ChildItem lists its contents
2111
2507
  if (\$prefix -and (Test-Path -Path \$lookupPrefix -PathType Container) -and -not \$prefix.EndsWith('/') -and -not \$prefix.EndsWith('\\')) {
@@ -2117,6 +2513,16 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
2117
2513
  # characters, use it as-is; otherwise append *
2118
2514
  \$globPath = if (\$lookupPrefix -match '[\*\?]') { \$lookupPrefix } else { "\${lookupPrefix}*" }
2119
2515
 
2516
+ # Keep the original native glob as an independent file filter
2517
+ # when the current word narrows its candidates.
2518
+ \$fixedMatches = @()
2519
+ if (\$narrowFixedGlob) {
2520
+ # Include hidden parents here; candidate enumeration still
2521
+ # controls which hidden children can be suggested.
2522
+ \$fixedMatches = @(Get-Item -Force -Path \$pattern -ErrorAction SilentlyContinue |
2523
+ Select-Object -ExpandProperty FullName)
2524
+ }
2525
+
2120
2526
  # Get file system items based on type
2121
2527
  \$items = @()
2122
2528
  switch (\$type) {
@@ -2169,6 +2575,14 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
2169
2575
  \$items = \$items | Where-Object { -not \$_.Attributes.HasFlag([System.IO.FileAttributes]::Hidden) }
2170
2576
  }
2171
2577
 
2578
+ if (\$narrowFixedGlob) {
2579
+ \$items = \$items | Where-Object {
2580
+ if (\$_.PSIsContainer) { \$true }
2581
+ elseif (\$IsWindows) { \$fixedMatches -contains \$_.FullName }
2582
+ else { \$fixedMatches -ccontains \$_.FullName }
2583
+ }
2584
+ }
2585
+
2172
2586
  # Extract directory prefix to preserve in completion text
2173
2587
  \$dirPrefix = if (\$prefix -and (\$prefix.Contains('/') -or \$prefix.Contains('\\'))) {
2174
2588
  \$slashIdx = [Math]::Max(\$prefix.LastIndexOf('/'), \$prefix.LastIndexOf('\\'))
@@ -805,12 +805,27 @@ 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
- local current="\$words[CURRENT]"
811
+ # Completion fields omit surrounding quotes, unlike words[CURRENT].
812
+ # Single-quoted fields are already literal; other fields retain escapes.
813
+ local current="\$IPREFIX\$PREFIX\$SUFFIX\$ISUFFIX"
814
+ local __logical_prefix="\$IPREFIX\$PREFIX"
811
815
  # Let zsh match the suffix when completing in the middle of a word.
812
816
  if [[ -o complete_in_word ]]; then
813
- current="\$PREFIX"
817
+ current="\$__logical_prefix"
818
+ fi
819
+ if [[ "\$compstate[quote]" == '\"' ]]; then
820
+ # Restore the omitted quoting context before removing shell escapes.
821
+ # In double quotes, a backslash before an ordinary character is literal.
822
+ current="\$QIPREFIX\$current\${QISUFFIX:-\$compstate[quote]}"
823
+ __logical_prefix="\$QIPREFIX\$__logical_prefix\$compstate[quote]"
824
+ current="\${(Q)current}"
825
+ __logical_prefix="\${(Q)__logical_prefix}"
826
+ elif [[ "\$compstate[quote]" != "'" ]]; then
827
+ current="\${(Q)current}"
828
+ __logical_prefix="\${(Q)__logical_prefix}"
814
829
  fi
815
830
  local -a prev
816
831
  # Extract previous arguments, skipping empty ones
@@ -818,7 +833,7 @@ function ${functionName} () {
818
833
  local i
819
834
  for (( i=2; i < CURRENT; i++ )); do
820
835
  if [[ -n "\$words[i]" ]]; then
821
- prev+=("\$words[i]")
836
+ prev+=("\${(Q)words[i]}")
822
837
  fi
823
838
  done
824
839
 
@@ -861,6 +876,9 @@ function ${functionName} () {
861
876
  unsetopt sh_glob
862
877
  local __was_extended_glob=0
863
878
  [[ -o extended_glob ]] && __was_extended_glob=1
879
+ local __was_sh_word_split=0
880
+ [[ -o sh_word_split ]] && __was_sh_word_split=1
881
+ unsetopt sh_word_split
864
882
 
865
883
  # Enable glob_dots when hidden files are requested so that
866
884
  # _files and _directories include dot-prefixed entries
@@ -873,6 +891,8 @@ function ${functionName} () {
873
891
  # current word. If the user has already typed beyond the pattern,
874
892
  # keep PREFIX unchanged for incremental narrowing.
875
893
  local __saved_prefix="\$PREFIX"
894
+ local __fixed_glob=0
895
+ local -a __fixed_matches
876
896
  local __initial_file_pattern="" __initial_directory="" __initial_leaf=""
877
897
  if [[ ( "\$type" == "file" || "\$type" == "any" || "\$type" == "directory" ) &&
878
898
  -z "\$current\$PREFIX\$SUFFIX" &&
@@ -900,20 +920,322 @@ function ${functionName} () {
900
920
  fi
901
921
  fi
902
922
  fi
903
- if [[ -n "\$pattern" && -z "\$__initial_file_pattern" ]]; then
923
+ local __saved_pattern_match="\$compstate[pattern_match]"
924
+ local __pattern_match_changed=0
925
+ # path() echoes current as its pattern. Keep zsh's escaped PREFIX
926
+ # in that case instead of reinterpreting logical text as shell syntax.
927
+ if [[ -n "\$pattern" && "\$pattern" != "\$current" && -z "\$__initial_file_pattern" ]]; then
904
928
  # Normalize leading ./ so that ./src/ma matches pattern src/
905
- local __norm_prefix="\${PREFIX#./}"
929
+ local __norm_prefix="\${__logical_prefix#./}"
906
930
  local __norm_pattern="\${pattern#./}"
907
931
  local __compare_pattern="\$__norm_pattern"
908
932
  if [[ "\$__compare_pattern" == *[\*\?]* ]]; then
909
933
  __compare_pattern="\${__compare_pattern%%[\*\?]*}"
910
934
  [[ "\$__compare_pattern" == */* ]] && __compare_pattern="\${__compare_pattern%/*}/" || __compare_pattern=""
911
935
  fi
912
- 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
936
+ if [[ ( -n "\$__compare_pattern" || -n "\$__norm_prefix" ) && \${#__norm_prefix} -ge \${#__compare_pattern} && "\${__norm_prefix[1,\${#__compare_pattern}]}" == "\$__compare_pattern" && "\$__logical_prefix" != "\$pattern" ]]; then
937
+ # Keep the typed prefix, but retain a fixed file glob separately.
938
+ if [[ "\$type" != "directory" && "\$__norm_pattern" == *[\*\?]* ]]; then
939
+ __fixed_glob=1
940
+ local __fixed_was_glob_dots=0
941
+ [[ -o glob_dots ]] && __fixed_was_glob_dots=1
942
+ if [[ "\$PREFIX" == .* || "\$PREFIX" == */.* ]]; then setopt glob_dots; fi
943
+ # Keep qualifiers in a string so sourcing also works with SH_GLOB.
944
+ local __fixed_pattern="" __glob_char="" __glob_previous=""
945
+ local __glob_escaped=0
946
+ local __glob_class=0 __glob_class_start=0 __glob_class_nested=""
947
+ local -i __glob_index
948
+ for (( __glob_index=1; __glob_index <= \${#pattern}; __glob_index++ )); do
949
+ __glob_char="\${pattern[__glob_index]}"
950
+ if [[ "\$__glob_class" == "1" ]]; then
951
+ # Class members are already literal. Preserve leading ]
952
+ # and nested POSIX classes without adding backslashes.
953
+ __fixed_pattern+="\$__glob_char"
954
+ if [[ -n "\$__glob_class_nested" ]]; then
955
+ if [[ "\$__glob_char" == "]" && "\$__glob_previous" == "\$__glob_class_nested" ]]; then
956
+ __glob_class_nested=""
957
+ fi
958
+ elif [[ "\$__glob_char" == "[" && "\${pattern[__glob_index+1]}" == ":" ]]; then
959
+ __glob_class_nested="\${pattern[__glob_index+1]}"
960
+ __glob_class_start=0
961
+ elif [[ "\$__glob_char" == "]" && "\$__glob_class_start" == "0" ]]; then
962
+ __glob_class=0
963
+ elif [[ "\$__glob_class_start" == "1" && ( "\$__glob_char" == "!" || "\$__glob_char" == "^" ) &&
964
+ ( "\${pattern[__glob_index+1]}" != "]" || "\${pattern[__glob_index+2,-1]}" == *']'* ) ]]; then
965
+ __glob_class_start=2
966
+ else
967
+ __glob_class_start=0
968
+ fi
969
+ __glob_previous="\$__glob_char"
970
+ continue
971
+ fi
972
+ if [[ "\$__glob_escaped" == "1" ]]; then
973
+ __fixed_pattern+="\$__glob_char"
974
+ __glob_escaped=0
975
+ __glob_previous=""
976
+ continue
977
+ fi
978
+ if [[ "\$__glob_char" == "\\\\" ]]; then
979
+ __fixed_pattern+="\$__glob_char"
980
+ __glob_escaped=1
981
+ elif [[ "\$__glob_char" == "[" ]]; then
982
+ __fixed_pattern+="\$__glob_char"
983
+ __glob_class=1
984
+ __glob_class_start=1
985
+ elif [[ "\$__glob_char" == [#^~\\(\\)\\|\\<\\>] &&
986
+ ! ( "\$__glob_char" == "~" && "\$__glob_index" == "1" ) ]]; then
987
+ # Keep class negation and leading home expansion; escape
988
+ # extended-glob operators in literal pathname components.
989
+ __fixed_pattern+="\\\\\$__glob_char"
990
+ else
991
+ __fixed_pattern+="\$__glob_char"
992
+ fi
993
+ __glob_previous="\$__glob_char"
994
+ done
995
+ __fixed_pattern+="(#qN)"
996
+ __fixed_matches=( \${~__fixed_pattern} )
997
+ __fixed_matches=( "\${(@)__fixed_matches:a}" )
998
+ # Hidden parents widen fixed matches, not native child suggestions.
999
+ if [[ "\$__fixed_was_glob_dots" == "0" && "\${PREFIX##*/}" != .* ]]; then unsetopt glob_dots; fi
1000
+ fi
915
1001
  else
916
- PREFIX="\$pattern"
1002
+ local __native_pattern="\$pattern"
1003
+ # Initial globs use the conservative native filters above.
1004
+ # Retained input and protected markers still need pattern matching.
1005
+ if [[ -n "\$compstate[context]" && "\$pattern" == *[\\*\\?\\[]* &&
1006
+ ( -n "\$current" || "\$pattern" == *'$'* || "\$pattern" == *'\`'* ) ]]; then
1007
+ compstate[pattern_match]='*'
1008
+ __pattern_match_changed=1
1009
+ fi
1010
+ # Native helpers expand substitutions, tilde names and executable
1011
+ # glob qualifiers. Input-derived patterns must keep these literal.
1012
+ # Ordinary fixed glob/tilde patterns retain their native behavior.
1013
+ local __tilde_name="\${pattern#\\~}"
1014
+ __tilde_name="\${__tilde_name%%/*}"
1015
+ local -a match mbegin mend
1016
+ local __code_qualifier=0
1017
+ local __fixed_qualifier_base=""
1018
+ local __safe_qualifiers='([-/F.@=p*rwxAIERWXsStUG^MTNDn,]|[ugdl]<->|[amc]([Mwhmsd]|)([-+]|)<->|L([kKmMgGtTpP]|)([-+]|)<->|[oO][nLlamcdN])##'
1019
+ local __qualifier_pattern="\$pattern" __qualifier
1020
+ while _have_glob_qual "\$__qualifier_pattern" complete; do
1021
+ __qualifier="\${match[5]#\\#q}"
1022
+ __qualifier_pattern="\$match[2]"
1023
+ # A numeric sense (+1) is not a function qualifier (+name).
1024
+ # Accept only bounded attribute/sort syntax in this exception.
1025
+ # Expand the pattern at runtime after unsetting SH_GLOB.
1026
+ if [[ "\$__qualifier" == \${~__safe_qualifiers} ]]; then
1027
+ __fixed_qualifier_base="\$__qualifier_pattern"
1028
+ elif [[ "\$__qualifier" == *[e+]* ]]; then
1029
+ __code_qualifier=1
1030
+ fi
1031
+ done
1032
+ # Partial overlaps with complete backend classes/groups remain
1033
+ # syntax. A complete input-provided expression can be literal.
1034
+ local -a __syntax_starts=() __syntax_ends=() __group_stack=()
1035
+ local -i __scan_index __class_begin=0 __member_begin=0
1036
+ local __scan_char __nested_class=""
1037
+ for (( __scan_index=1; __scan_index <= \${#pattern}; __scan_index++ )); do
1038
+ __scan_char="\${pattern[__scan_index]}"
1039
+ if [[ "\$__scan_char" == '\\' ]]; then
1040
+ (( __scan_index++ ))
1041
+ elif (( __class_begin )); then
1042
+ if [[ "\$__scan_char" == '[' && "\${pattern[__scan_index+1]}" == [:.=] ]]; then
1043
+ __nested_class="\${pattern[__scan_index+1]}"
1044
+ elif [[ -n "\$__nested_class" && "\$__scan_char" == "\$__nested_class" && "\${pattern[__scan_index+1]}" == ']' ]]; then
1045
+ (( __scan_index++ ))
1046
+ __nested_class=""
1047
+ elif [[ -z "\$__nested_class" && "\$__scan_char" == ']' ]] && (( __scan_index > __member_begin )); then
1048
+ __syntax_starts+=("\$__class_begin")
1049
+ __syntax_ends+=("\$__scan_index")
1050
+ __class_begin=0
1051
+ elif (( __scan_index == __member_begin )) &&
1052
+ [[ "\$__scan_char" == '!' || "\$__scan_char" == '^' ]] &&
1053
+ [[ "\${pattern[__scan_index+1]}" != ']' || "\${pattern[__scan_index+2,-1]}" == *']'* ]]; then
1054
+ (( __member_begin++ ))
1055
+ fi
1056
+ elif [[ "\$__scan_char" == '[' ]]; then
1057
+ __class_begin=\$__scan_index
1058
+ __member_begin=\$(( __scan_index + 1 ))
1059
+ elif [[ "\$__scan_char" == '(' ]]; then
1060
+ __group_stack+=("\$__scan_index")
1061
+ elif [[ "\$__scan_char" == ')' ]] && (( \${#__group_stack} )); then
1062
+ __syntax_starts+=("\${__group_stack[-1]}")
1063
+ __syntax_ends+=("\$__scan_index")
1064
+ __group_stack[-1]=()
1065
+ fi
1066
+ done
1067
+ # Search retained input only where it cannot split fixed syntax.
1068
+ local __input_part="\$current"
1069
+ local __backend_prefix="\$pattern" __backend_suffix=""
1070
+ local __search_pattern __candidate_prefix
1071
+ local -i __search_offset __overlap_start __overlap_end __range_index __syntax_overlap
1072
+ local -i __fixed_tilde_end=0
1073
+ local __tilde_expression='([+-]|[+-]<->|[[:alnum:]_.-]#)'
1074
+ if [[ "\$pattern" == '~'* &&
1075
+ "\$__tilde_name" == \${~__tilde_expression} ]]; then
1076
+ __fixed_tilde_end=\$(( \${#__tilde_name} + 1 ))
1077
+ fi
1078
+ while [[ -n "\$__input_part" ]]; do
1079
+ __search_pattern="\$pattern"
1080
+ __search_offset=0
1081
+ while [[ "\$__search_pattern" == *"\$__input_part"* ]]; do
1082
+ __candidate_prefix="\${__search_pattern%%"\$__input_part"*}"
1083
+ __overlap_start=\$(( __search_offset + \${#__candidate_prefix} + 1 ))
1084
+ __overlap_end=\$(( __overlap_start + \${#__input_part} - 1 ))
1085
+ __syntax_overlap=0
1086
+ # Leading home/directory-stack/named-directory syntax belongs
1087
+ # to the fixed pattern, even when the input overlaps it.
1088
+ if (( __fixed_tilde_end && __overlap_start <= __fixed_tilde_end )); then
1089
+ __syntax_overlap=1
1090
+ fi
1091
+ for (( __range_index=1; __range_index <= \${#__syntax_starts}; __range_index++ )); do
1092
+ if (( __overlap_start <= __syntax_ends[__range_index] &&
1093
+ __overlap_end >= __syntax_starts[__range_index] &&
1094
+ ( __overlap_start > __syntax_starts[__range_index] ||
1095
+ __overlap_end < __syntax_ends[__range_index] ) )); then
1096
+ __syntax_overlap=1
1097
+ break
1098
+ fi
1099
+ done
1100
+ # Every position in a recognized fixed attribute qualifier
1101
+ # is backend syntax, including its closing parenthesis.
1102
+ if [[ "\$__fixed_qualifier_base" == *[\\*\\?\\]] ]] &&
1103
+ (( __overlap_end > \${#__fixed_qualifier_base} )); then
1104
+ __syntax_overlap=1
1105
+ fi
1106
+ if (( ! __syntax_overlap )); then
1107
+ if (( __overlap_start == 1 )); then
1108
+ __backend_prefix=""
1109
+ else
1110
+ __backend_prefix="\${pattern[1,__overlap_start-1]}"
1111
+ fi
1112
+ __backend_suffix="\${pattern[__overlap_end+1,-1]}"
1113
+ break 2
1114
+ fi
1115
+ __search_offset=\$__overlap_start
1116
+ __search_pattern="\${pattern[__overlap_start+1,-1]}"
1117
+ done
1118
+ __input_part="\${__input_part[2,-1]}"
1119
+ done
1120
+ local __quoted_input="\${(q)__input_part}"
1121
+ # Pattern matching removes quoting differently from normal
1122
+ # completion. Character classes keep these characters literal.
1123
+ if [[ -n "\$compstate[pattern_match]" ]]; then
1124
+ __quoted_input="\${(b)__input_part}"
1125
+ __quoted_input="\${__quoted_input//\\\$/[\\\$]}"
1126
+ __quoted_input="\${__quoted_input//\\\`/[\\\`]}"
1127
+ fi
1128
+ if [[ "\$pattern" == *'$'* || "\$pattern" == *'\`'* ||
1129
+ ( "\$pattern" == '~'* && "\$__tilde_name" == *[^[:alnum:]_.+-]* ) ]] ||
1130
+ (( __code_qualifier )); then
1131
+ __native_pattern="\${(q)pattern}"
1132
+ local __backend_pattern="\$__backend_prefix\$__backend_suffix"
1133
+ local __backend_tilde="\${__backend_pattern#\\~}"
1134
+ __backend_tilde="\${__backend_tilde%%/*}"
1135
+ local __unsafe_backend_qualifier=0
1136
+ __qualifier_pattern="\$__backend_pattern"
1137
+ while _have_glob_qual "\$__qualifier_pattern" complete; do
1138
+ __qualifier="\${match[5]#\\#q}"
1139
+ __qualifier_pattern="\$match[2]"
1140
+ if [[ "\$__qualifier" != \${~__safe_qualifiers} ]]; then
1141
+ __unsafe_backend_qualifier=1
1142
+ fi
1143
+ done
1144
+ # Retain trusted wildcards and safe attribute qualifiers when
1145
+ # all executable syntax belongs to the quoted input.
1146
+ if [[ -n "\$__input_part" &&
1147
+ "\$__backend_pattern" != *'$'* &&
1148
+ "\$__backend_pattern" != *'\`'* &&
1149
+ ( "\$__backend_pattern" != '~'* ||
1150
+ "\$__backend_tilde" != *[^[:alnum:]_.+-]* ) ]] &&
1151
+ (( ! __unsafe_backend_qualifier )); then
1152
+ __native_pattern="\$__backend_prefix\$__quoted_input\$__backend_suffix"
1153
+ elif [[ "\$__backend_pattern" != *'$('* &&
1154
+ "\$__backend_pattern" != *'\${'* &&
1155
+ "\$__backend_pattern" != *'$['* &&
1156
+ ( "\$pattern" != '~'* ||
1157
+ "\$__tilde_name" != *[^[:alnum:]_.+-]* ) ]] &&
1158
+ (( ! __unsafe_backend_qualifier )); then
1159
+ # Fixed patterns also need literal substitution markers,
1160
+ # without quoting their wildcards or safe qualifiers.
1161
+ local __dollar='\\\$'
1162
+ local __backtick='\\\`'
1163
+ if [[ -n "\$compstate[pattern_match]" ]]; then
1164
+ __dollar='[\$]'
1165
+ __backtick='[\`]'
1166
+ fi
1167
+ # Existing glob classes already protect their markers.
1168
+ local -a __backend_parts=()
1169
+ local __part __escaped __char __class_kind
1170
+ local -i __index __in_class __class_start
1171
+ for __part in "\$__backend_prefix" "\$__backend_suffix"; do
1172
+ __escaped=""
1173
+ __in_class=0
1174
+ __class_kind=""
1175
+ for (( __index=1; __index <= \${#__part}; __index++ )); do
1176
+ __char="\${__part[__index]}"
1177
+ if [[ "\$__char" == '\\' ]]; then
1178
+ __escaped+="\$__char\${__part[__index+1]}"
1179
+ (( __index++ ))
1180
+ continue
1181
+ elif (( __in_class )); then
1182
+ if [[ "\$__char" == '[' && "\${__part[__index+1]}" == [:.=] ]]; then
1183
+ __class_kind="\${__part[__index+1]}"
1184
+ elif [[ -n "\$__class_kind" && "\$__char" == "\$__class_kind" && "\${__part[__index+1]}" == ']' ]]; then
1185
+ __escaped+="\$__char]"
1186
+ (( __index++ ))
1187
+ __class_kind=""
1188
+ continue
1189
+ elif [[ -z "\$__class_kind" && "\$__char" == ']' ]] && (( __index > __class_start )); then
1190
+ __in_class=0
1191
+ elif (( __index == __class_start )) && [[ "\$__char" == '!' || "\$__char" == '^' ]]; then
1192
+ (( __class_start++ ))
1193
+ fi
1194
+ elif [[ "\$__char" == '[' ]]; then
1195
+ __in_class=1
1196
+ __class_start=$(( __index + 1 ))
1197
+ elif [[ "\$__char" == '$' ]]; then
1198
+ __char="\$__dollar"
1199
+ elif [[ "\$__char" == '\`' ]]; then
1200
+ __char="\$__backtick"
1201
+ fi
1202
+ __escaped+="\$__char"
1203
+ done
1204
+ __backend_parts+=("\$__escaped")
1205
+ done
1206
+ __backend_prefix="\${__backend_parts[1]}"
1207
+ __backend_suffix="\${__backend_parts[2]}"
1208
+ __native_pattern="\$__backend_prefix\$__quoted_input\$__backend_suffix"
1209
+ fi
1210
+ else
1211
+ # Quote retained metacharacters, including segments made only
1212
+ # of them. Wildcard-only overlap remains backend syntax.
1213
+ if [[ "\$__input_part" == *[\\\\\\(\\)\\[\\#\\^\\|\\~\\<\\>\\*\\?]* &&
1214
+ "\$__input_part" != [\\*\\?]## ]]; then
1215
+ __native_pattern="\$__backend_prefix\$__quoted_input\$__backend_suffix"
1216
+ fi
1217
+ fi
1218
+ PREFIX="\$__native_pattern"
1219
+ # Match simple fixed tilde globs through native -g filters. The
1220
+ # pattern matcher otherwise treats the leading ~ as glob syntax,
1221
+ # even after _path_files has expanded it for filesystem lookup.
1222
+ if (( __fixed_tilde_end )) && [[ -z "\$__input_part" &&
1223
+ "\$__native_pattern" == "\$pattern" && "\$pattern" == */* ]]; then
1224
+ __initial_directory="\${pattern%/*}/"
1225
+ __initial_leaf="\${pattern##*/}"
1226
+ if [[ "\$__initial_leaf" == *[\*\?]* &&
1227
+ "\$__initial_directory" != *[\*\?]* &&
1228
+ "\$__initial_directory" != *["[]()"]* &&
1229
+ "\$__initial_leaf" != *["[]()#~^|<>;&"]* &&
1230
+ "\$__initial_leaf" != *'\\'* &&
1231
+ "\$__initial_leaf" != *[[:space:]]* &&
1232
+ "\$__initial_leaf" != *"'"* && "\$__initial_leaf" != *'"'* ]]; then
1233
+ __initial_file_pattern="\$__initial_leaf"
1234
+ PREFIX="\$__initial_directory"
1235
+ compstate[pattern_match]=""
1236
+ __pattern_match_changed=1
1237
+ fi
1238
+ fi
917
1239
  fi
918
1240
  fi
919
1241
 
@@ -935,6 +1257,9 @@ function ${functionName} () {
935
1257
  # Regular-file suggestions retain symlinked files; initial
936
1258
  # any globs use a non-directory qualifier instead.
937
1259
  local file_pattern="\${ext_pattern:-*}(#q-.)"
1260
+ if [[ "\$__fixed_glob" == "1" ]]; then
1261
+ file_pattern="\${ext_pattern:-*}(#q-.e:${fixedGlobFunction}:)"
1262
+ fi
938
1263
  if [[ -n "\$__initial_file_pattern" ]]; then
939
1264
  local __file_qualifier="-."
940
1265
  [[ "\$type" == "any" ]] && __file_qualifier="^-/"
@@ -950,6 +1275,21 @@ function ${functionName} () {
950
1275
  fi
951
1276
  file_pattern="\${(j: :)__initial_file_patterns}"
952
1277
  fi
1278
+ local file_pattern_prefix="\$PREFIX"
1279
+ local -a match mbegin mend
1280
+ # Native qualifier copying concatenates flags with the explicit
1281
+ # regular-file group. Keep backend attributes in their own group.
1282
+ if [[ -n "\$compstate[pattern_match]" && -z "\$SUFFIX" ]]; then
1283
+ local file_qualifier file_qualifiers=""
1284
+ while _have_glob_qual "\$file_pattern_prefix" complete; do
1285
+ file_qualifier="\${match[5]#\\#q}"
1286
+ file_pattern_prefix="\$match[2]"
1287
+ file_qualifiers="(#q\$file_qualifier)\$file_qualifiers"
1288
+ done
1289
+ if [[ -n "\$file_qualifiers" ]]; then
1290
+ file_pattern+="\$file_qualifiers"
1291
+ fi
1292
+ fi
953
1293
  local file_subtree_pattern="**/*(#q/N)"
954
1294
  local -a file_subtree=() file_recursive_patterns=()
955
1295
  local -a file_tags=(files directories)
@@ -963,15 +1303,22 @@ function ${functionName} () {
963
1303
  local file_matched=0
964
1304
  for file_tag in "\${file_tags[@]}"; do
965
1305
  if [[ "\$file_tag" == directories ]]; then
1306
+ local file_tag_prefix="\$PREFIX"
1307
+ PREFIX="\$file_pattern_prefix"
966
1308
  while _next_label directories expl directory; do
1309
+ # File qualifiers constrain final results, not the
1310
+ # directories needed to navigate to those results.
967
1311
  _path_files -/ "\${expl[@]}" && file_matched=1
968
1312
  done
1313
+ PREFIX="\$file_tag_prefix"
969
1314
  else
1315
+ local file_tag_prefix="\$PREFIX"
1316
+ PREFIX="\$file_pattern_prefix"
970
1317
  while _next_label files expl file; do
971
1318
  local -a file_ignore=() file_ignore_args=()
972
1319
  # Explicit globs bypass automatic fignore. Apply it only
973
1320
  # 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
1321
+ if [[ -z "\$__initial_file_pattern" && -z "\$ext_pattern" && ( "\$__fixed_glob" != "1" || "\$pattern" == "*" ) && -z "\$_comp_no_ignore" && -n "\$FIGNORE" ]] && (( \${#_comp_ignore[@]} == 0 )); then
975
1322
  file_ignore=("?*\${^fignore[@]}")
976
1323
  file_ignore_args=(-F file_ignore)
977
1324
  fi
@@ -997,12 +1344,17 @@ function ${functionName} () {
997
1344
  done
998
1345
  fi
999
1346
  done
1347
+ PREFIX="\$file_tag_prefix"
1000
1348
  fi
1001
1349
  done
1002
1350
  (( file_matched )) && break
1003
1351
  done
1004
- elif [[ -n "\$ext_pattern" ]]; then
1352
+ elif [[ -n "\$ext_pattern" || "\$__fixed_glob" == "1" ]]; then
1005
1353
  local file_pattern="\${ext_pattern}(#q^-/)"
1354
+ if [[ "\$__fixed_glob" == "1" ]]; then
1355
+ # Put the predicate before ^, which negates later qualifiers.
1356
+ file_pattern="\${ext_pattern:-*}(#qe:${fixedGlobFunction}:^-/)"
1357
+ fi
1006
1358
  _wanted files expl file _path_files -g "\${file_pattern}"
1007
1359
  _wanted directories expl directory _path_files -/
1008
1360
  else
@@ -1021,7 +1373,11 @@ function ${functionName} () {
1021
1373
  # Restore PREFIX and glob_dots to their previous state
1022
1374
  PREFIX="\$__saved_prefix"
1023
1375
  if [[ "\$__was_extended_glob" == "1" ]]; then setopt extended_glob; else unsetopt extended_glob; fi
1376
+ if (( __pattern_match_changed )); then
1377
+ compstate[pattern_match]="\$__saved_pattern_match"
1378
+ fi
1024
1379
  if [[ "\$__was_sh_glob" == "1" ]]; then setopt sh_glob; else unsetopt sh_glob; fi
1380
+ if [[ "\$__was_sh_word_split" == "1" ]]; then setopt sh_word_split; else unsetopt sh_word_split; fi
1025
1381
  if [[ "\$__was_glob_dots" == "1" ]]; then setopt glob_dots; else unsetopt glob_dots; fi
1026
1382
  else
1027
1383
  # Regular literal completion
@@ -1038,20 +1394,31 @@ function ${functionName} () {
1038
1394
  # Prepare completion with descriptions for _describe
1039
1395
  local -a matches
1040
1396
  local -i i
1397
+ local __match
1041
1398
  for (( i=1; i <= \${#completions[@]}; i++ )); do
1042
- # _describe treats colons as separators and backslashes as escapes.
1043
- value="\${completions[i]//\\\\/\\\\\\\\}"
1044
- value="\${value//:/\\\\:}"
1399
+ # _describe uses backslashes to escape its colon separator.
1400
+ __match="\${completions[i]//\\\\/\\\\\\\\}"
1401
+ __match="\${__match//:/\\\\:}"
1045
1402
  if [[ -n "\${descriptions[i]}" ]]; then
1046
- matches+=("\${value}:\${descriptions[i]}")
1403
+ matches+=("\$__match:\${descriptions[i]}")
1047
1404
  else
1048
- matches+=("\${value}")
1405
+ matches+=("\$__match")
1049
1406
  fi
1050
1407
  done
1051
- _describe 'commands' matches
1408
+ if [[ -n "\$QISUFFIX" ]]; then
1409
+ # A closing quote already exists; do not append a space inside it.
1410
+ _describe 'commands' matches -S ''
1411
+ else
1412
+ _describe 'commands' matches
1413
+ fi
1052
1414
  fi
1053
1415
  }
1054
1416
 
1417
+ function ${fixedGlobFunction} () {
1418
+ local __fixed_candidate="\${REPLY:a}"
1419
+ (( \${__fixed_matches[(Ie)\$__fixed_candidate]} ))
1420
+ }
1421
+
1055
1422
  if (( $+functions[compdef] )); then
1056
1423
  compdef ${functionName} ${programName}
1057
1424
  fi
@@ -1685,12 +2052,12 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
1685
2052
  # prefix for incremental narrowing. Normalize path separators
1686
2053
  # before comparing so that Windows backslashes match forward slashes
1687
2054
  # in the transported pattern.
2055
+ let is_win = (($nu.os-info.name | str downcase) == "windows")
1688
2056
  let glob_base = if ($pattern | is-not-empty) {
1689
2057
  # Normalize separators and leading ./; downcase only on Windows
1690
2058
  # where filesystems are typically case-insensitive
1691
2059
  let norm_prefix_raw = ($prefix | str replace -a '\\' '/' | str replace -r '^\\./' '')
1692
2060
  let norm_pattern_raw = ($pattern | str replace -a '\\' '/' | str replace -r '^\\./' '')
1693
- let is_win = (($nu.os-info.name | str downcase) == "windows")
1694
2061
  let norm_prefix = (if $is_win { $norm_prefix_raw | str downcase } else { $norm_prefix_raw })
1695
2062
  let norm_pattern = (if $is_win { $norm_pattern_raw | str downcase } else { $norm_pattern_raw })
1696
2063
  # For wildcard patterns, compare only the directory prefix
@@ -1709,6 +2076,21 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
1709
2076
  $prefix
1710
2077
  }
1711
2078
 
2079
+ # Intersect fixed glob matches with the narrowed prefix candidates.
2080
+ # Directory-only suggestions retain their existing navigation behavior.
2081
+ let narrow_fixed_glob = ($type != "directory") and ($pattern =~ '[*?]') and ($prefix != $pattern) and ($glob_base == $prefix)
2082
+ let fixed_matches = if $narrow_fixed_glob {
2083
+ try {
2084
+ let fixed_pattern = ($pattern | into glob)
2085
+ let include_hidden = $hidden or ($prefix | path basename | str starts-with '.')
2086
+ let matches = if $include_hidden { ls -a --directory $fixed_pattern } else { ls --directory $fixed_pattern }
2087
+ $matches | each {|item|
2088
+ let name = ($env.PWD | path join $item.name | path expand --no-symlink)
2089
+ if $is_win { $name | str downcase } else { $name }
2090
+ }
2091
+ } catch { [] }
2092
+ } else { [] }
2093
+
1712
2094
  # If the glob base is a directory without a trailing slash (e.g., "src"),
1713
2095
  # append one so that ls enumerates its contents instead of matching
1714
2096
  # siblings like "src-old". path expand follows symlinks, so that
@@ -1831,6 +2213,17 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
1831
2213
  }
1832
2214
  }
1833
2215
 
2216
+ let filtered = if $narrow_fixed_glob {
2217
+ $filtered | where {|item|
2218
+ # Any-mode listings already include directory symlinks; keep them
2219
+ # navigable without changing their existing completion rendering.
2220
+ let navigation_dir = ($item.type == dir) or ($item.type == symlink and (try { ($item.name | path expand | path type) == "dir" } catch { false }))
2221
+ let name = ($env.PWD | path join $item.name | path expand --no-symlink)
2222
+ let normalized_name = if $is_win { $name | str downcase } else { $name }
2223
+ $navigation_dir or ($normalized_name in $fixed_matches)
2224
+ }
2225
+ } else { $filtered }
2226
+
1834
2227
  # Extract directory prefix to preserve in completion text
1835
2228
  let dir_prefix = if ($glob_base | is-empty) {
1836
2229
  ""
@@ -2106,6 +2499,9 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
2106
2499
  \$prefixFromInput = \$valuePrefix -and \$prefix -ceq \$valuePrefix
2107
2500
  \$lookupPrefix = if (\$prefixFromInput -and \$null -ne \$quoteType -and \$prefix.StartsWith('~')) { './' + \$prefix } else { \$prefix }
2108
2501
 
2502
+ # Detect narrowing before directory normalization changes prefix.
2503
+ \$narrowFixedGlob = \$type -ne 'directory' -and \$pattern -match '[\*\?]' -and \$prefix -ceq \$valuePrefix -and \$pattern -cne \$valuePrefix
2504
+
2109
2505
  # If prefix is a directory without trailing slash, append
2110
2506
  # one so Get-ChildItem lists its contents
2111
2507
  if (\$prefix -and (Test-Path -Path \$lookupPrefix -PathType Container) -and -not \$prefix.EndsWith('/') -and -not \$prefix.EndsWith('\\')) {
@@ -2117,6 +2513,16 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
2117
2513
  # characters, use it as-is; otherwise append *
2118
2514
  \$globPath = if (\$lookupPrefix -match '[\*\?]') { \$lookupPrefix } else { "\${lookupPrefix}*" }
2119
2515
 
2516
+ # Keep the original native glob as an independent file filter
2517
+ # when the current word narrows its candidates.
2518
+ \$fixedMatches = @()
2519
+ if (\$narrowFixedGlob) {
2520
+ # Include hidden parents here; candidate enumeration still
2521
+ # controls which hidden children can be suggested.
2522
+ \$fixedMatches = @(Get-Item -Force -Path \$pattern -ErrorAction SilentlyContinue |
2523
+ Select-Object -ExpandProperty FullName)
2524
+ }
2525
+
2120
2526
  # Get file system items based on type
2121
2527
  \$items = @()
2122
2528
  switch (\$type) {
@@ -2169,6 +2575,14 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
2169
2575
  \$items = \$items | Where-Object { -not \$_.Attributes.HasFlag([System.IO.FileAttributes]::Hidden) }
2170
2576
  }
2171
2577
 
2578
+ if (\$narrowFixedGlob) {
2579
+ \$items = \$items | Where-Object {
2580
+ if (\$_.PSIsContainer) { \$true }
2581
+ elseif (\$IsWindows) { \$fixedMatches -contains \$_.FullName }
2582
+ else { \$fixedMatches -ccontains \$_.FullName }
2583
+ }
2584
+ }
2585
+
2172
2586
  # Extract directory prefix to preserve in completion text
2173
2587
  \$dirPrefix = if (\$prefix -and (\$prefix.Contains('/') -or \$prefix.Contains('\\'))) {
2174
2588
  \$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.2887",
4
4
  "description": "Type-safe combinatorial command-line interface parser",
5
5
  "keywords": [
6
6
  "CLI",