@optique/core 1.0.0-dev.1163 → 1.0.0-dev.1172

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.
@@ -99,6 +99,50 @@ function _${programName} () {
99
99
  fi
100
100
  fi
101
101
 
102
+ # When a pattern is specified, use it as the glob base instead of the
103
+ # current word so that completions enumerate the pattern's directory.
104
+ # However, if the user has already typed beyond the pattern (e.g.,
105
+ # pattern="src/" and current="src/ma"), preserve the typed suffix
106
+ # for incremental filtering.
107
+ local __from_pattern=0
108
+ if [[ -n "$pattern" ]]; then
109
+ # Normalize leading ./ so that ./src/ma matches pattern src/
110
+ local __norm_current="\${current#./}"
111
+ local __norm_pattern="\${pattern#./}"
112
+ # For wildcard patterns, compare only the directory prefix before
113
+ # any wildcards so that typing src/ma narrows src/*.ts correctly
114
+ local __compare_pattern="$__norm_pattern"
115
+ if [[ "$__compare_pattern" == *[\*\?]* ]]; then
116
+ __compare_pattern="\${__compare_pattern%%[\*\?]*}"
117
+ [[ "$__compare_pattern" == */* ]] && __compare_pattern="\${__compare_pattern%/*}/" || __compare_pattern=""
118
+ fi
119
+ if [[ ( -n "$__compare_pattern" || -n "$__norm_current" ) && \${#__norm_current} -ge \${#__compare_pattern} && "\${__norm_current:0:\${#__compare_pattern}}" == "$__compare_pattern" && "$current" != "$pattern" ]]; then
120
+ # User has typed beyond or an equivalent form of the pattern
121
+ true
122
+ else
123
+ __from_pattern=1
124
+ # Reset tilde state from the current-word expansion so that a
125
+ # non-tilde pattern is not rewritten through stale tilde state
126
+ __tilde_prefix=""
127
+ __tilde_expanded=""
128
+ __glob_current="$pattern"
129
+ if [[ "$pattern" =~ ^(~[a-zA-Z0-9_.+-]*)(/.*)?$ ]]; then
130
+ __tilde_prefix="\${BASH_REMATCH[1]}"
131
+ eval "__tilde_expanded=\$__tilde_prefix" 2>/dev/null || true
132
+ if [[ -n "$__tilde_expanded" && "$__tilde_expanded" != "$__tilde_prefix" ]]; then
133
+ __glob_current="\${__tilde_expanded}\${pattern#\$__tilde_prefix}"
134
+ else
135
+ __tilde_prefix=""
136
+ fi
137
+ fi
138
+ # If the glob base is a directory without a trailing slash,
139
+ # append one so that the glob enumerates its contents
140
+ if [[ -d "$__glob_current" && "$__glob_current" != */ ]]; then
141
+ __glob_current="$__glob_current/"
142
+ fi
143
+ fi
144
+ fi
145
+
102
146
  # Enable dotglob when hidden files are requested, or when the user
103
147
  # is already navigating inside a hidden directory (e.g., ~/.config/nvim/)
104
148
  # This runs after tilde expansion so that paths like ~/.config/ are
@@ -114,6 +158,36 @@ function _${programName} () {
114
158
  [[ "$__prefix_base" == .* ]] && __prefix_targets_hidden=1
115
159
  if [[ "$hidden" == "1" || "$__inside_hidden_path" == "1" || "$__prefix_targets_hidden" == "1" ]]; then shopt -s dotglob; fi
116
160
 
161
+ # Pre-expand file candidates. When the glob base came from the
162
+ # program's pattern and contains wildcard characters (* or ?),
163
+ # use it as-is via compgen -G (safe — no command substitution).
164
+ # Otherwise append * to treat it as a prefix.
165
+ # Note: [ is NOT treated as a glob indicator because it commonly
166
+ # appears in literal filenames like [draft] or foo[1].txt.
167
+ local -a __candidates=()
168
+ if [[ "$__from_pattern" == "1" && "$__glob_current" == *[\*\?]* ]]; then
169
+ mapfile -t __candidates < <(compgen -G "$__glob_current" 2>/dev/null)
170
+ # For file/any modes, also include directories from the base
171
+ # directory for navigation even when the glob itself does not
172
+ # match directory names. Skip this for directory mode so that
173
+ # the pattern's basename filter is respected.
174
+ if [[ "$type" != "directory" ]]; then
175
+ local __glob_dir="\${__glob_current%/*}"
176
+ [[ "$__glob_dir" == "$__glob_current" ]] && __glob_dir="."
177
+ local __d
178
+ for __d in "$__glob_dir"/*/; do
179
+ [[ -d "$__d" ]] && __candidates+=("\${__d%/}")
180
+ done
181
+ fi
182
+ else
183
+ __candidates=("$__glob_current"*)
184
+ # Remove no-match sentinel (bash returns the literal pattern).
185
+ # Also check -L for dangling symlinks which are valid candidates.
186
+ if [[ \${#__candidates[@]} -eq 1 && ! -e "\${__candidates[0]}" && ! -L "\${__candidates[0]}" ]]; then
187
+ __candidates=()
188
+ fi
189
+ fi
190
+
117
191
  # Generate file completions based on type
118
192
  case "$type" in
119
193
  file)
@@ -121,7 +195,7 @@ function _${programName} () {
121
195
  if [[ -n "$extensions" ]]; then
122
196
  # Files with extension filtering + directories
123
197
  local ext_pattern="\${extensions//,/|}"
124
- for item in "$__glob_current"*; do
198
+ for item in "\${__candidates[@]}"; do
125
199
  if [[ -d "$item" ]]; then
126
200
  COMPREPLY+=("$item/")
127
201
  elif [[ -f "$item" && "$item" =~ \\.($ext_pattern)$ ]]; then
@@ -130,7 +204,7 @@ function _${programName} () {
130
204
  done
131
205
  else
132
206
  # Complete files and directories for navigation
133
- for item in "$__glob_current"*; do
207
+ for item in "\${__candidates[@]}"; do
134
208
  if [[ -d "$item" ]]; then
135
209
  COMPREPLY+=("$item/")
136
210
  elif [[ -f "$item" ]]; then
@@ -141,7 +215,7 @@ function _${programName} () {
141
215
  ;;
142
216
  directory)
143
217
  # Complete directories only
144
- for dir in "$__glob_current"*; do
218
+ for dir in "\${__candidates[@]}"; do
145
219
  [[ -d "$dir" ]] && COMPREPLY+=("$dir/")
146
220
  done
147
221
  ;;
@@ -151,7 +225,7 @@ function _${programName} () {
151
225
  # Files with extension filtering + directories
152
226
  # Files with extension filtering
153
227
  local ext_pattern="\${extensions//,/|}"
154
- for item in "$__glob_current"*; do
228
+ for item in "\${__candidates[@]}"; do
155
229
  if [[ -d "$item" ]]; then
156
230
  COMPREPLY+=("$item/")
157
231
  elif [[ ( -e "$item" || -L "$item" ) && "$item" =~ \\.($ext_pattern)$ ]]; then
@@ -160,7 +234,7 @@ function _${programName} () {
160
234
  done
161
235
  else
162
236
  # Complete files and directories, add slash to directories
163
- for item in "$__glob_current"*; do
237
+ for item in "\${__candidates[@]}"; do
164
238
  if [[ -d "$item" ]]; then
165
239
  COMPREPLY+=("$item/")
166
240
  # Use -e || -L to include non-regular files (sockets, FIFOs, dangling symlinks)
@@ -278,6 +352,28 @@ function _${programName.replace(/[^a-zA-Z0-9]/g, "_")} () {
278
352
  [[ -o glob_dots ]] && __was_glob_dots=1
279
353
  if [[ "\$hidden" == "1" ]]; then setopt glob_dots; fi
280
354
 
355
+ # When a pattern is specified, override PREFIX so that _files and
356
+ # _directories enumerate the pattern's directory instead of the
357
+ # current word. If the user has already typed beyond the pattern,
358
+ # keep PREFIX unchanged for incremental narrowing.
359
+ local __saved_prefix="\$PREFIX"
360
+ if [[ -n "\$pattern" ]]; then
361
+ # Normalize leading ./ so that ./src/ma matches pattern src/
362
+ local __norm_prefix="\${PREFIX#./}"
363
+ local __norm_pattern="\${pattern#./}"
364
+ local __compare_pattern="\$__norm_pattern"
365
+ if [[ "\$__compare_pattern" == *[\*\?]* ]]; then
366
+ __compare_pattern="\${__compare_pattern%%[\*\?]*}"
367
+ [[ "\$__compare_pattern" == */* ]] && __compare_pattern="\${__compare_pattern%/*}/" || __compare_pattern=""
368
+ fi
369
+ if [[ ( -n "\$__compare_pattern" || -n "\$__norm_prefix" ) && \${#__norm_prefix} -ge \${#__compare_pattern} && "\${__norm_prefix[1,\${#__compare_pattern}]}" == "\$__compare_pattern" && "\$PREFIX" != "\$pattern" ]]; then
370
+ # User typed an equivalent or extended form — keep PREFIX
371
+ true
372
+ else
373
+ PREFIX="\$pattern"
374
+ fi
375
+ fi
376
+
281
377
  # Use zsh's native file completion
282
378
  case "\$type" in
283
379
  file)
@@ -303,7 +399,8 @@ function _${programName.replace(/[^a-zA-Z0-9]/g, "_")} () {
303
399
  ;;
304
400
  esac
305
401
 
306
- # Restore glob_dots to its previous state
402
+ # Restore PREFIX and glob_dots to their previous state
403
+ PREFIX="\$__saved_prefix"
307
404
  if [[ "\$__was_glob_dots" == "1" ]]; then setopt glob_dots; else unsetopt glob_dots; fi
308
405
  else
309
406
  # Regular literal completion
@@ -382,12 +479,118 @@ ${escapedArgs ? ` set -l output (${programName} ${escapedArgs} $prev $current
382
479
  set -l pattern (string replace -a '%25' '%' -- (string replace -a '%3A' ':' -- $parts[4]))
383
480
  set -l hidden $parts[5]
384
481
 
482
+ # When a pattern is specified, use it as the glob base instead
483
+ # of the current word. If the user has already typed beyond the
484
+ # pattern (e.g., pattern="src/" and current="src/ma"), keep the
485
+ # current word for incremental narrowing.
486
+ set -l glob_base $current
487
+ set -l __tilde_prefix ""
488
+ set -l __from_pattern 0
489
+ if test -n "$pattern"
490
+ # Normalize leading ./ so that ./src/ma matches pattern src/
491
+ set -l __norm_current (string replace -r '^\\./' '' -- "$current")
492
+ set -l __norm_pattern (string replace -r '^\\./' '' -- "$pattern")
493
+ # For wildcard patterns, compare only the directory prefix
494
+ set -l __compare_pattern "$__norm_pattern"
495
+ if string match -q '*[*?]*' -- "$__compare_pattern"
496
+ set __compare_pattern (string replace -r '[*?].*' '' -- "$__compare_pattern")
497
+ if string match -q '*/*' -- "$__compare_pattern"
498
+ set __compare_pattern (string replace -r '/[^/]*$' '/' -- "$__compare_pattern")
499
+ else
500
+ set __compare_pattern ""
501
+ end
502
+ end
503
+ set -l __cp_len (string length -- "$__compare_pattern")
504
+ set -l __nc_len (string length -- "$__norm_current")
505
+ if begin; test -n "$__compare_pattern"; or test -n "$__norm_current"; end
506
+ and test $__nc_len -ge $__cp_len
507
+ and test (string sub -l $__cp_len -- "$__norm_current") = "$__compare_pattern"
508
+ and test "$current" != "$pattern"
509
+ set glob_base $current
510
+ else
511
+ set glob_base $pattern
512
+ set __from_pattern 1
513
+ end
514
+ end
515
+
516
+ # Expand tilde prefix for globbing — fish does not expand ~
517
+ # inside variable substitutions, so replace it with $HOME
518
+ if string match -q '~/*' -- "$glob_base"
519
+ set __tilde_prefix "~"
520
+ set glob_base (string replace -r '^~' "$HOME" -- "$glob_base")
521
+ else if string match -q '~' -- "$glob_base"
522
+ set __tilde_prefix "~"
523
+ set glob_base "$HOME/"
524
+ end
525
+
526
+ # If the glob base is a directory without a trailing slash,
527
+ # append one so that the glob enumerates its contents
528
+ if test -d "$glob_base"; and not string match -q '*/' -- "$glob_base"
529
+ set glob_base "$glob_base/"
530
+ end
531
+
532
+ # Pre-expand file candidates. When the glob base came from
533
+ # the program's pattern and contains * or ?, use it as the
534
+ # complete glob expression. Otherwise append * as a prefix.
535
+ # Note: [ is NOT treated as a glob because it commonly
536
+ # appears in literal filenames like [draft] or foo[1].txt.
537
+ set -l __has_glob 0
538
+ if test $__from_pattern -eq 1
539
+ and string match -q '*[*?]*' -- "$glob_base"
540
+ set __has_glob 1
541
+ end
542
+ set -l __candidates
543
+ if test $__has_glob -eq 1
544
+ # Safe glob expansion without eval: split the pattern into
545
+ # directory and filter parts, list the directory, then use
546
+ # string match for wildcard filtering
547
+ set -l __glob_dir (string replace -r '/[^/]*$' '' -- "$glob_base")
548
+ set -l __glob_filter (string replace -r '.*/' '' -- "$glob_base")
549
+ if test -z "$__glob_dir"; or test "$__glob_dir" = "$glob_base"
550
+ set __glob_dir "."
551
+ end
552
+ # Match files by the glob filter. Fish's * does not match
553
+ # dotfiles, so also scan .* when the filter targets them.
554
+ for __item in $__glob_dir/*
555
+ if test -e "$__item"
556
+ set -l __bn (basename "$__item")
557
+ if string match -q "$__glob_filter" -- "$__bn"
558
+ set -a __candidates "$__item"
559
+ end
560
+ end
561
+ end
562
+ if string match -q '.*' -- "$__glob_filter"
563
+ for __item in $__glob_dir/.*
564
+ if test -e "$__item"
565
+ set -l __bn (basename "$__item")
566
+ if test "$__bn" = "." -o "$__bn" = ".."
567
+ continue
568
+ end
569
+ if string match -q "$__glob_filter" -- "$__bn"
570
+ set -a __candidates "$__item"
571
+ end
572
+ end
573
+ end
574
+ end
575
+ # For file/any modes, also include directories for navigation.
576
+ # Skip for directory mode so the pattern filter is respected.
577
+ if test "$type" != "directory"
578
+ for __item in $__glob_dir/*/
579
+ if test -d "$__item"
580
+ set -a __candidates (string replace -r '/$' '' -- "$__item")
581
+ end
582
+ end
583
+ end
584
+ else
585
+ set __candidates $glob_base*
586
+ end
587
+
385
588
  # Generate file completions based on type
386
589
  set -l items
387
590
  switch $type
388
591
  case file
389
592
  # Complete files and directories (directories for navigation)
390
- for item in $current*
593
+ for item in $__candidates
391
594
  if test -d $item
392
595
  set -a items $item/
393
596
  else if test -f $item
@@ -395,14 +598,14 @@ ${escapedArgs ? ` set -l output (${programName} ${escapedArgs} $prev $current
395
598
  end
396
599
  end
397
600
  # Fish's * glob does not match dotfiles; add them
398
- # explicitly when the basename is empty (i.e., $current
601
+ # explicitly when the basename is empty (i.e., $glob_base
399
602
  # is "" or ends with "/"), because only then are * and
400
603
  # .* complementary. When a non-empty basename is present
401
604
  # (e.g., "foo"), foo* already covers foo.txt, so foo.*
402
605
  # would just produce duplicates.
403
- if test "$hidden" = "1"
404
- if test -z "$current"; or string match -q '*/' -- "$current"
405
- for item in $current.*
606
+ if test "$hidden" = "1" -a $__has_glob -eq 0
607
+ if test -z "$glob_base"; or string match -q '*/' -- "$glob_base"
608
+ for item in $glob_base.*
406
609
  if test -d $item
407
610
  set -a items $item/
408
611
  else if test -f $item
@@ -413,14 +616,14 @@ ${escapedArgs ? ` set -l output (${programName} ${escapedArgs} $prev $current
413
616
  end
414
617
  case directory
415
618
  # Complete directories only
416
- for item in $current*
619
+ for item in $__candidates
417
620
  if test -d $item
418
621
  set -a items $item/
419
622
  end
420
623
  end
421
- if test "$hidden" = "1"
422
- if test -z "$current"; or string match -q '*/' -- "$current"
423
- for item in $current.*
624
+ if test "$hidden" = "1" -a $__has_glob -eq 0
625
+ if test -z "$glob_base"; or string match -q '*/' -- "$glob_base"
626
+ for item in $glob_base.*
424
627
  if test -d $item
425
628
  set -a items $item/
426
629
  end
@@ -429,16 +632,16 @@ ${escapedArgs ? ` set -l output (${programName} ${escapedArgs} $prev $current
429
632
  end
430
633
  case any
431
634
  # Complete both files and directories
432
- for item in $current*
635
+ for item in $__candidates
433
636
  if test -d $item
434
637
  set -a items $item/
435
638
  else if test -f $item
436
639
  set -a items $item
437
640
  end
438
641
  end
439
- if test "$hidden" = "1"
440
- if test -z "$current"; or string match -q '*/' -- "$current"
441
- for item in $current.*
642
+ if test "$hidden" = "1" -a $__has_glob -eq 0
643
+ if test -z "$glob_base"; or string match -q '*/' -- "$glob_base"
644
+ for item in $glob_base.*
442
645
  if test -d $item
443
646
  set -a items $item/
444
647
  else if test -f $item
@@ -470,8 +673,11 @@ ${escapedArgs ? ` set -l output (${programName} ${escapedArgs} $prev $current
470
673
  set items $filtered
471
674
  end
472
675
 
473
- # Filter out hidden files unless requested
474
- if test "$hidden" != "1" -a (string sub -l 1 -- $current) != "."
676
+ # Filter out hidden files unless requested.
677
+ # Check the basename of glob_base so that patterns like
678
+ # "src/.e" correctly target hidden entries.
679
+ set -l __glob_basename (string replace -r '.*/(.*)' '$1' -- $glob_base)
680
+ if test "$hidden" != "1" -a (string sub -l 1 -- $__glob_basename) != "."
475
681
  set -l filtered
476
682
  for item in $items
477
683
  set -l basename (basename $item)
@@ -482,6 +688,15 @@ ${escapedArgs ? ` set -l output (${programName} ${escapedArgs} $prev $current
482
688
  set items $filtered
483
689
  end
484
690
 
691
+ # Restore tilde prefix in completion results
692
+ if test -n "$__tilde_prefix"
693
+ set -l restored
694
+ for item in $items
695
+ set -a restored (string replace "$HOME" "~" -- $item)
696
+ end
697
+ set items $restored
698
+ end
699
+
485
700
  # Output file completions
486
701
  for item in $items
487
702
  echo $item
@@ -635,10 +850,67 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
635
850
  ""
636
851
  }
637
852
 
638
- # Generate file completions based on type
639
- # Use current directory if prefix is empty
853
+ # When a pattern is specified, use it as the glob base instead of
854
+ # the user-typed prefix. If the user has already typed beyond the
855
+ # pattern (e.g., pattern="src/" and prefix="src/ma"), keep the
856
+ # prefix for incremental narrowing. Normalize path separators
857
+ # before comparing so that Windows backslashes match forward slashes
858
+ # in the transported pattern.
859
+ let glob_base = if ($pattern | is-not-empty) {
860
+ # Normalize separators and leading ./; downcase only on Windows
861
+ # where filesystems are typically case-insensitive
862
+ let norm_prefix_raw = ($prefix | str replace -a '\\' '/' | str replace -r '^\\./' '')
863
+ let norm_pattern_raw = ($pattern | str replace -a '\\' '/' | str replace -r '^\\./' '')
864
+ let is_win = (($nu.os-info.name | str downcase) == "windows")
865
+ let norm_prefix = (if $is_win { $norm_prefix_raw | str downcase } else { $norm_prefix_raw })
866
+ let norm_pattern = (if $is_win { $norm_pattern_raw | str downcase } else { $norm_pattern_raw })
867
+ # For wildcard patterns, compare only the directory prefix
868
+ let compare_pattern = if ($norm_pattern =~ '[*?]') {
869
+ let before_wild = ($norm_pattern | str replace -r '[*?].*' '')
870
+ if ($before_wild =~ '/') { $before_wild | str replace -r '/[^/]*$' '/' } else { "" }
871
+ } else {
872
+ $norm_pattern
873
+ }
874
+ if (($compare_pattern | is-not-empty) or ($norm_prefix | is-not-empty)) and ($norm_prefix | str starts-with $compare_pattern) and (($norm_prefix | str length) >= ($compare_pattern | str length)) and ($prefix != $pattern) {
875
+ $prefix
876
+ } else {
877
+ $pattern
878
+ }
879
+ } else {
880
+ $prefix
881
+ }
882
+
883
+ # If the glob base is a directory without a trailing slash (e.g., "src"),
884
+ # append one so that ls enumerates its contents instead of matching
885
+ # siblings like "src-old"
886
+ let glob_base = if ($glob_base | is-not-empty) and ($glob_base | path type) == "dir" and (not ($glob_base | str ends-with "/")) {
887
+ $glob_base + "/"
888
+ } else {
889
+ $glob_base
890
+ }
891
+
892
+ # Generate file completions based on type.
893
+ # When the glob base contains wildcard characters, use it as-is;
894
+ # otherwise append * to treat it as a prefix.
640
895
  # Note: into glob is required so that ls expands wildcards from a variable
641
- let ls_pattern = if ($prefix | is-empty) { "." } else { ($prefix + "*" | into glob) }
896
+ let has_glob = ($glob_base =~ '[*?]')
897
+ let ls_pattern = if ($glob_base | is-empty) {
898
+ "."
899
+ } else if $has_glob {
900
+ ($glob_base | into glob)
901
+ } else {
902
+ ($glob_base + "*" | into glob)
903
+ }
904
+
905
+ # When using a glob pattern, also compute a directory listing pattern
906
+ # so that file/any modes can include directories for navigation
907
+ let glob_dir_pattern = if $has_glob {
908
+ let dir_part = ($glob_base | path dirname)
909
+ let dir = if ($dir_part | is-empty) or ($dir_part == $glob_base) { "." } else { $dir_part }
910
+ ($dir + "/*" | into glob)
911
+ } else {
912
+ null
913
+ }
642
914
 
643
915
  # Use ls -a to include hidden files when requested
644
916
  let items = try {
@@ -679,8 +951,25 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
679
951
  []
680
952
  }
681
953
 
682
- # Filter out hidden files unless requested
683
- let filtered = if $hidden or ($prefix | str starts-with '.') {
954
+ # When using a glob pattern, also add directories from the base
955
+ # directory for navigation (file/any modes should list directories
956
+ # even when the glob itself does not match directory names)
957
+ let items = if ($glob_dir_pattern != null) and ($type != "directory") {
958
+ let extra_dirs = try {
959
+ (if $hidden { ls -a $glob_dir_pattern } else { ls $glob_dir_pattern }) | where type == dir
960
+ } catch {
961
+ []
962
+ }
963
+ $items | append $extra_dirs | uniq-by name
964
+ } else {
965
+ $items
966
+ }
967
+
968
+ # Filter out hidden files unless requested.
969
+ # Check the basename of glob_base so that patterns like "src/.e"
970
+ # correctly target hidden entries.
971
+ let glob_basename = ($glob_base | path basename)
972
+ let filtered = if $hidden or ($glob_basename | str starts-with '.') {
684
973
  $items
685
974
  } else {
686
975
  $items | where {|item|
@@ -690,12 +979,12 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
690
979
  }
691
980
 
692
981
  # Extract directory prefix to preserve in completion text
693
- let dir_prefix = if ($prefix | is-empty) {
982
+ let dir_prefix = if ($glob_base | is-empty) {
694
983
  ""
695
- } else if ($prefix | str ends-with "/") {
696
- $prefix
984
+ } else if ($glob_base | str ends-with "/") {
985
+ $glob_base
697
986
  } else {
698
- let parsed = ($prefix | path parse)
987
+ let parsed = ($glob_base | path parse)
699
988
  if ($parsed.parent | is-empty) { "" } else if ($parsed.parent | str ends-with "/") { $parsed.parent } else { $parsed.parent + "/" }
700
989
  }
701
990
 
@@ -836,11 +1125,47 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
836
1125
  \$pattern = \$parts[3] -replace '%3A', ':' -replace '%25', '%'
837
1126
  \$hidden = \$parts[4] -eq '1'
838
1127
 
839
- # Determine current prefix for file matching
840
- \$prefix = if (\$wordToComplete) { \$wordToComplete } else { '' }
1128
+ # When a pattern is specified, use it as the file matching
1129
+ # prefix instead of the current word. If the user has
1130
+ # already typed beyond the pattern, keep their input for
1131
+ # incremental narrowing. Normalize path separators before
1132
+ # comparing so that Windows backslashes match forward slashes
1133
+ # in the transported pattern.
1134
+ \$normalizedPattern = if (\$pattern) { \$pattern.Replace('\\', '/') -replace '^\\./','' } else { '' }
1135
+ \$normalizedWord = if (\$wordToComplete) { \$wordToComplete.Replace('\\', '/') -replace '^\\./','' } else { '' }
1136
+ \$comparison = if (\$IsWindows) { [System.StringComparison]::OrdinalIgnoreCase } else { [System.StringComparison]::Ordinal }
1137
+ # For wildcard patterns, compare only the directory prefix
1138
+ \$comparePattern = \$normalizedPattern
1139
+ if (\$comparePattern -match '[\*\?]') {
1140
+ \$beforeWild = \$comparePattern -replace '[\*\?].*', ''
1141
+ if (\$beforeWild.Contains('/')) {
1142
+ \$comparePattern = \$beforeWild.Substring(0, \$beforeWild.LastIndexOf('/') + 1)
1143
+ } else {
1144
+ \$comparePattern = ''
1145
+ }
1146
+ }
1147
+ \$prefix = if ((\$comparePattern -or \$normalizedWord) -and \$normalizedWord -and \$normalizedWord.StartsWith(\$comparePattern, \$comparison) -and \$normalizedWord.Length -ge \$comparePattern.Length -and \$wordToComplete -ne \$pattern) {
1148
+ \$wordToComplete
1149
+ } elseif (\$pattern) {
1150
+ \$pattern
1151
+ } elseif (\$wordToComplete) {
1152
+ \$wordToComplete
1153
+ } else { '' }
1154
+
1155
+ # Use -Force to include hidden files when requested, or when
1156
+ # the prefix basename targets dotfiles (e.g., src/.e)
1157
+ \$prefixBasename = Split-Path -Leaf \$prefix 2>\$null
1158
+ \$forceParam = if (\$hidden -or (\$prefixBasename -and \$prefixBasename.StartsWith('.'))) { @{Force = \$true} } else { @{} }
1159
+
1160
+ # If prefix is a directory without trailing slash, append
1161
+ # one so Get-ChildItem lists its contents
1162
+ if (\$prefix -and (Test-Path -Path \$prefix -PathType Container) -and -not \$prefix.EndsWith('/') -and -not \$prefix.EndsWith('\\')) {
1163
+ \$prefix = \$prefix + '/'
1164
+ }
841
1165
 
842
- # Use -Force to include hidden files when requested
843
- \$forceParam = if (\$hidden) { @{Force = \$true} } else { @{} }
1166
+ # Build the glob path when the prefix contains wildcard
1167
+ # characters, use it as-is; otherwise append *
1168
+ \$globPath = if (\$prefix -match '[\*\?]') { \$prefix } else { "\${prefix}*" }
844
1169
 
845
1170
  # Get file system items based on type
846
1171
  \$items = @()
@@ -849,37 +1174,46 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
849
1174
  if (\$extensions) {
850
1175
  # Filter by extensions, always include directories
851
1176
  \$extList = \$extensions -split ','
852
- \$items = Get-ChildItem @forceParam -Path "\${prefix}*" -ErrorAction SilentlyContinue |
1177
+ \$items = Get-ChildItem @forceParam -Path \$globPath -ErrorAction SilentlyContinue |
853
1178
  Where-Object {
854
1179
  if (\$_.PSIsContainer) { return \$true }
855
1180
  \$ext = \$_.Extension
856
1181
  \$extList | ForEach-Object { if (\$ext -eq ".\$_") { return \$true } }
857
1182
  }
858
1183
  } else {
859
- \$items = Get-ChildItem @forceParam -Path "\${prefix}*" -ErrorAction SilentlyContinue
1184
+ \$items = Get-ChildItem @forceParam -Path \$globPath -ErrorAction SilentlyContinue
860
1185
  }
861
1186
  }
862
1187
  'directory' {
863
- \$items = Get-ChildItem @forceParam -Directory -Path "\${prefix}*" -ErrorAction SilentlyContinue
1188
+ \$items = Get-ChildItem @forceParam -Directory -Path \$globPath -ErrorAction SilentlyContinue
864
1189
  }
865
1190
  'any' {
866
1191
  if (\$extensions) {
867
1192
  # Filter by extensions, always include directories
868
1193
  \$extList = \$extensions -split ','
869
- \$items = Get-ChildItem @forceParam -Path "\${prefix}*" -ErrorAction SilentlyContinue |
1194
+ \$items = Get-ChildItem @forceParam -Path \$globPath -ErrorAction SilentlyContinue |
870
1195
  Where-Object {
871
1196
  if (\$_.PSIsContainer) { return \$true }
872
1197
  \$ext = \$_.Extension
873
1198
  \$extList | ForEach-Object { if (\$ext -eq ".\$_") { return \$true } }
874
1199
  }
875
1200
  } else {
876
- \$items = Get-ChildItem @forceParam -Path "\${prefix}*" -ErrorAction SilentlyContinue
1201
+ \$items = Get-ChildItem @forceParam -Path \$globPath -ErrorAction SilentlyContinue
877
1202
  }
878
1203
  }
879
1204
  }
880
1205
 
881
- # Filter hidden files unless requested
882
- if (-not \$hidden) {
1206
+ # For file/any modes with glob patterns, also add directories
1207
+ # from the base directory for navigation
1208
+ if (\$prefix -match '[\*\?]' -and \$type -ne 'directory') {
1209
+ \$globDir = Split-Path -Parent \$prefix
1210
+ if (-not \$globDir) { \$globDir = '.' }
1211
+ \$extraDirs = Get-ChildItem @forceParam -Directory -Path "\$globDir/*" -ErrorAction SilentlyContinue
1212
+ if (\$extraDirs) { \$items = @(\$items) + @(\$extraDirs) | Select-Object -Unique }
1213
+ }
1214
+
1215
+ # Filter hidden files unless requested or the prefix targets dotfiles
1216
+ if (-not \$hidden -and -not (\$prefixBasename -and \$prefixBasename.StartsWith('.'))) {
883
1217
  \$items = \$items | Where-Object { -not \$_.Attributes.HasFlag([System.IO.FileAttributes]::Hidden) }
884
1218
  }
885
1219