@optique/core 0.10.7 → 1.0.0-dev.1109

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.
Files changed (56) hide show
  1. package/README.md +4 -6
  2. package/dist/annotations.cjs +209 -1
  3. package/dist/annotations.d.cts +78 -1
  4. package/dist/annotations.d.ts +78 -1
  5. package/dist/annotations.js +201 -1
  6. package/dist/completion.cjs +186 -50
  7. package/dist/completion.js +186 -50
  8. package/dist/constructs.cjs +310 -78
  9. package/dist/constructs.d.cts +525 -644
  10. package/dist/constructs.d.ts +525 -644
  11. package/dist/constructs.js +311 -79
  12. package/dist/context.cjs +43 -3
  13. package/dist/context.d.cts +113 -5
  14. package/dist/context.d.ts +113 -5
  15. package/dist/context.js +41 -3
  16. package/dist/dependency.cjs +172 -66
  17. package/dist/dependency.d.cts +22 -2
  18. package/dist/dependency.d.ts +22 -2
  19. package/dist/dependency.js +172 -66
  20. package/dist/doc.cjs +46 -1
  21. package/dist/doc.d.cts +24 -0
  22. package/dist/doc.d.ts +24 -0
  23. package/dist/doc.js +46 -1
  24. package/dist/facade.cjs +702 -322
  25. package/dist/facade.d.cts +124 -190
  26. package/dist/facade.d.ts +124 -190
  27. package/dist/facade.js +703 -323
  28. package/dist/index.cjs +5 -0
  29. package/dist/index.d.cts +5 -5
  30. package/dist/index.d.ts +5 -5
  31. package/dist/index.js +3 -3
  32. package/dist/message.cjs +7 -4
  33. package/dist/message.js +7 -4
  34. package/dist/mode-dispatch.cjs +23 -1
  35. package/dist/mode-dispatch.d.cts +55 -0
  36. package/dist/mode-dispatch.d.ts +55 -0
  37. package/dist/mode-dispatch.js +21 -1
  38. package/dist/modifiers.cjs +210 -55
  39. package/dist/modifiers.js +211 -56
  40. package/dist/parser.cjs +80 -47
  41. package/dist/parser.d.cts +18 -3
  42. package/dist/parser.d.ts +18 -3
  43. package/dist/parser.js +82 -50
  44. package/dist/primitives.cjs +102 -37
  45. package/dist/primitives.d.cts +81 -24
  46. package/dist/primitives.d.ts +81 -24
  47. package/dist/primitives.js +103 -39
  48. package/dist/usage.cjs +88 -6
  49. package/dist/usage.d.cts +51 -13
  50. package/dist/usage.d.ts +51 -13
  51. package/dist/usage.js +85 -7
  52. package/dist/valueparser.cjs +371 -99
  53. package/dist/valueparser.d.cts +56 -7
  54. package/dist/valueparser.d.ts +56 -7
  55. package/dist/valueparser.js +371 -99
  56. package/package.json +10 -1
@@ -5,16 +5,17 @@ import { formatMessage } from "./message.js";
5
5
  * A regular expression pattern for valid program names that can be safely
6
6
  * interpolated into shell scripts.
7
7
  *
8
- * This pattern allows:
8
+ * The first character must be alphanumeric or underscore. Subsequent
9
+ * characters may also include hyphens and dots:
9
10
  * - Letters (a-z, A-Z)
10
11
  * - Numbers (0-9)
11
12
  * - Underscore (_)
12
- * - Hyphen (-)
13
- * - Dot (.)
13
+ * - Hyphen (-) — not as the first character
14
+ * - Dot (.) — not as the first character
14
15
  *
15
16
  * @internal
16
17
  */
17
- const SAFE_PROGRAM_NAME_PATTERN = /^[a-zA-Z0-9_.-]+$/;
18
+ const SAFE_PROGRAM_NAME_PATTERN = /^[a-zA-Z0-9_][a-zA-Z0-9_.-]*$/;
18
19
  /**
19
20
  * Validates a program name for safe use in shell scripts.
20
21
  *
@@ -27,7 +28,17 @@ const SAFE_PROGRAM_NAME_PATTERN = /^[a-zA-Z0-9_.-]+$/;
27
28
  * @internal
28
29
  */
29
30
  function validateProgramName(programName) {
30
- if (!SAFE_PROGRAM_NAME_PATTERN.test(programName)) throw new Error(`Invalid program name for shell completion: "${programName}". Program names must contain only alphanumeric characters, underscores, hyphens, and dots.`);
31
+ if (!SAFE_PROGRAM_NAME_PATTERN.test(programName)) throw new Error(`Invalid program name for shell completion: "${programName}". Program names must start with an alphanumeric character or underscore, and contain only alphanumeric characters, underscores, hyphens, and dots.`);
32
+ }
33
+ /**
34
+ * Percent-encodes colons and percent signs in a pattern string so that it
35
+ * can be safely embedded in the colon-delimited `__FILE__` transport format.
36
+ * @param pattern The raw pattern string.
37
+ * @returns The encoded pattern string.
38
+ * @internal
39
+ */
40
+ function encodePattern(pattern) {
41
+ return pattern.replace(/%/g, "%25").replace(/:/g, "%3A");
31
42
  }
32
43
  /**
33
44
  * The Bash shell completion generator.
@@ -47,6 +58,47 @@ function _${programName} () {
47
58
  if [[ "$line" == __FILE__:* ]]; then
48
59
  # Parse file completion directive: __FILE__:type:extensions:pattern:hidden
49
60
  IFS=':' read -r _ type extensions pattern hidden <<< "$line"
61
+ pattern="\${pattern//%3A/:}"; pattern="\${pattern//%25/%}"
62
+
63
+ # Save and adjust glob/shell options for safe file completion
64
+ local __dotglob_was_set=0 __failglob_was_set=0 __noglob_was_set=0
65
+ local __globignore_was_set=0 __saved_globignore="\${GLOBIGNORE-}"
66
+ [[ \${GLOBIGNORE+x} == x ]] && __globignore_was_set=1
67
+ shopt -q dotglob && __dotglob_was_set=1
68
+ shopt -q failglob && __failglob_was_set=1
69
+ [[ $- == *f* ]] && __noglob_was_set=1
70
+ # Unset GLOBIGNORE before enabling dotglob because unsetting
71
+ # GLOBIGNORE implicitly clears dotglob in Bash
72
+ shopt -u failglob 2>/dev/null
73
+ set +f
74
+ unset GLOBIGNORE
75
+
76
+ # Expand tilde prefix for file globbing
77
+ local __glob_current="$current" __tilde_prefix="" __tilde_expanded=""
78
+ if [[ "$current" =~ ^(~[a-zA-Z0-9_.+-]*)(/.*)$ ]]; then
79
+ __tilde_prefix="\${BASH_REMATCH[1]}"
80
+ eval "__tilde_expanded=\$__tilde_prefix" 2>/dev/null || true
81
+ if [[ -n "$__tilde_expanded" && "$__tilde_expanded" != "$__tilde_prefix" ]]; then
82
+ __glob_current="\${__tilde_expanded}\${current#\$__tilde_prefix}"
83
+ else
84
+ __tilde_prefix=""
85
+ fi
86
+ fi
87
+
88
+ # Enable dotglob when hidden files are requested, or when the user
89
+ # is already navigating inside a hidden directory (e.g., ~/.config/nvim/)
90
+ # This runs after tilde expansion so that paths like ~/.config/ are
91
+ # checked against the expanded path, not the literal ~ string.
92
+ local __inside_hidden_path=0
93
+ case "/\${__glob_current%/}/" in
94
+ */.[!.]*/*|*/..?*/*) __inside_hidden_path=1 ;;
95
+ esac
96
+ # Also check if the current prefix explicitly targets hidden entries
97
+ # (e.g., user typed "." or ".e" to complete .env)
98
+ local __prefix_targets_hidden=0
99
+ local __prefix_base="\${__glob_current##*/}"
100
+ [[ "$__prefix_base" == .* ]] && __prefix_targets_hidden=1
101
+ if [[ "$hidden" == "1" || "$__inside_hidden_path" == "1" || "$__prefix_targets_hidden" == "1" ]]; then shopt -s dotglob; fi
50
102
 
51
103
  # Generate file completions based on type
52
104
  case "$type" in
@@ -55,21 +107,21 @@ function _${programName} () {
55
107
  if [[ -n "$extensions" ]]; then
56
108
  # Complete with extension filtering
57
109
  local ext_pattern="\${extensions//,/|}"
58
- for file in "$current"*; do
59
- [[ -e "$file" && "$file" =~ \\.($ext_pattern)$ ]] && COMPREPLY+=("$file")
110
+ for file in "$__glob_current"*; do
111
+ [[ -f "$file" && "$file" =~ \\.($ext_pattern)$ ]] && COMPREPLY+=("$file")
60
112
  done
61
113
  else
62
114
  # Complete files only, exclude directories
63
- while IFS= read -r -d '' item; do
115
+ for item in "$__glob_current"*; do
64
116
  [[ -f "$item" ]] && COMPREPLY+=("$item")
65
- done < <(compgen -f -z -- "$current")
117
+ done
66
118
  fi
67
119
  ;;
68
120
  directory)
69
121
  # Complete directories only
70
- while IFS= read -r -d '' dir; do
71
- COMPREPLY+=("$dir/")
72
- done < <(compgen -d -z -- "$current")
122
+ for dir in "$__glob_current"*; do
123
+ [[ -d "$dir" ]] && COMPREPLY+=("$dir/")
124
+ done
73
125
  ;;
74
126
  any)
75
127
  # Complete both files and directories
@@ -77,31 +129,50 @@ function _${programName} () {
77
129
  # Files with extension filtering + directories
78
130
  # Files with extension filtering
79
131
  local ext_pattern="\${extensions//,/|}"
80
- for item in "$current"*; do
132
+ for item in "$__glob_current"*; do
81
133
  if [[ -d "$item" ]]; then
82
134
  COMPREPLY+=("$item/")
83
- elif [[ -f "$item" && "$item" =~ \\.($ext_pattern)$ ]]; then
135
+ elif [[ ( -e "$item" || -L "$item" ) && "$item" =~ \\.($ext_pattern)$ ]]; then
84
136
  COMPREPLY+=("$item")
85
137
  fi
86
138
  done
87
139
  else
88
140
  # Complete files and directories, add slash to directories
89
- while IFS= read -r -d '' item; do
141
+ for item in "$__glob_current"*; do
90
142
  if [[ -d "$item" ]]; then
91
143
  COMPREPLY+=("$item/")
92
- else
144
+ # Use -e || -L to include non-regular files (sockets, FIFOs, dangling symlinks)
145
+ elif [[ -e "$item" || -L "$item" ]]; then
93
146
  COMPREPLY+=("$item")
94
147
  fi
95
- done < <(compgen -f -z -- "$current")
148
+ done
96
149
  fi
97
150
  ;;
98
151
  esac
99
152
 
153
+ # Restore tilde prefix in completion results
154
+ if [[ -n "$__tilde_prefix" ]]; then
155
+ local __i
156
+ for __i in "\${!COMPREPLY[@]}"; do
157
+ COMPREPLY[\$__i]="\${COMPREPLY[\$__i]/#\$__tilde_expanded/\$__tilde_prefix}"
158
+ done
159
+ fi
160
+
161
+ # Restore glob/shell options
162
+ # Restore GLOBIGNORE before dotglob because assigning GLOBIGNORE
163
+ # implicitly enables dotglob in Bash
164
+ if [[ "$__globignore_was_set" == "1" ]]; then GLOBIGNORE="$__saved_globignore"; fi
165
+ if [[ "$__dotglob_was_set" == "0" ]]; then shopt -u dotglob; else shopt -s dotglob; fi
166
+ if [[ "$__failglob_was_set" == "1" ]]; then shopt -s failglob; fi
167
+ if [[ "$__noglob_was_set" == "1" ]]; then set -f; fi
168
+
100
169
  # Filter out hidden files unless requested
101
- if [[ "$hidden" != "1" && "$current" != .* ]]; then
170
+ if [[ "$hidden" != "1" && "$__inside_hidden_path" == "0" && "$__prefix_targets_hidden" == "0" ]]; then
102
171
  local filtered=()
172
+ local __name
103
173
  for item in "\${COMPREPLY[@]}"; do
104
- [[ "$(basename "$item")" != .* ]] && filtered+=("$item")
174
+ __name="\${item%/}"; __name="\${__name##*/}"
175
+ [[ "$__name" != .* ]] && filtered+=("$item")
105
176
  done
106
177
  COMPREPLY=("\${filtered[@]}")
107
178
  fi
@@ -123,7 +194,8 @@ complete -F _${programName} ${programName}
123
194
  else {
124
195
  const extensions = suggestion.extensions?.join(",") || "";
125
196
  const hidden = suggestion.includeHidden ? "1" : "0";
126
- yield `__FILE__:${suggestion.type}:${extensions}:${suggestion.pattern || ""}:${hidden}`;
197
+ const pattern = encodePattern(suggestion.pattern ?? "");
198
+ yield `__FILE__:${suggestion.type}:${extensions}:${pattern}:${hidden}`;
127
199
  }
128
200
  i++;
129
201
  }
@@ -175,6 +247,7 @@ function _${programName.replace(/[^a-zA-Z0-9]/g, "_")} () {
175
247
  # Parse file completion directive: __FILE__:type:extensions:pattern:hidden
176
248
  local type extensions pattern hidden
177
249
  IFS=':' read -r _ type extensions pattern hidden <<< "\$value"
250
+ pattern="\${pattern//%3A/:}"; pattern="\${pattern//%25/%}"
178
251
  has_file_completion=1
179
252
 
180
253
  # Use zsh's native file completion
@@ -182,8 +255,8 @@ function _${programName.replace(/[^a-zA-Z0-9]/g, "_")} () {
182
255
  file)
183
256
  if [[ -n "\$extensions" ]]; then
184
257
  # Complete files with extension filtering
185
- local ext_pattern="*.(\\$\{extensions//,/|\})"
186
- _files -g "\\$ext_pattern"
258
+ local ext_pattern="*.(\$\{extensions//,/|\})"
259
+ _files -g "\$ext_pattern"
187
260
  else
188
261
  _files -g "*"
189
262
  fi
@@ -194,8 +267,8 @@ function _${programName.replace(/[^a-zA-Z0-9]/g, "_")} () {
194
267
  any)
195
268
  if [[ -n "\$extensions" ]]; then
196
269
  # Complete both files and directories, with extension filtering for files
197
- local ext_pattern="*.(\\$\{extensions//,/|\})"
198
- _files -g "\\$ext_pattern" && _directories
270
+ local ext_pattern="*.(\$\{extensions//,/|\})"
271
+ _files -g "\$ext_pattern" && _directories
199
272
  else
200
273
  _files
201
274
  fi
@@ -241,7 +314,8 @@ compdef _${programName.replace(/[^a-zA-Z0-9]/g, "_")} ${programName}
241
314
  const extensions = suggestion.extensions?.join(",") || "";
242
315
  const hidden = suggestion.includeHidden ? "1" : "0";
243
316
  const description = suggestion.description == null ? "" : formatMessage(suggestion.description, { colors: false });
244
- yield `__FILE__:${suggestion.type}:${extensions}:${suggestion.pattern || ""}:${hidden}\0${description}\0`;
317
+ const pattern = encodePattern(suggestion.pattern ?? "");
318
+ yield `__FILE__:${suggestion.type}:${extensions}:${pattern}:${hidden}\0${description}\0`;
245
319
  }
246
320
  }
247
321
  };
@@ -273,10 +347,11 @@ ${escapedArgs ? ` set -l output (${programName} ${escapedArgs} $prev $current
273
347
  for line in $output
274
348
  if string match -q '__FILE__:*' -- $line
275
349
  # Parse file completion directive: __FILE__:type:extensions:pattern:hidden
276
- set -l parts (string split ':' -- $line)
350
+ set -l directive (string split \\t -- $line)[1]
351
+ set -l parts (string split ':' -- $directive)
277
352
  set -l type $parts[2]
278
353
  set -l extensions $parts[3]
279
- set -l pattern $parts[4]
354
+ set -l pattern (string replace -a '%25' '%' -- (string replace -a '%3A' ':' -- $parts[4]))
280
355
  set -l hidden $parts[5]
281
356
 
282
357
  # Generate file completions based on type
@@ -289,6 +364,21 @@ ${escapedArgs ? ` set -l output (${programName} ${escapedArgs} $prev $current
289
364
  set -a items $item
290
365
  end
291
366
  end
367
+ # Fish's * glob does not match dotfiles; add them
368
+ # explicitly when the basename is empty (i.e., $current
369
+ # is "" or ends with "/"), because only then are * and
370
+ # .* complementary. When a non-empty basename is present
371
+ # (e.g., "foo"), foo* already covers foo.txt, so foo.*
372
+ # would just produce duplicates.
373
+ if test "$hidden" = "1"
374
+ if test -z "$current"; or string match -q '*/' -- "$current"
375
+ for item in $current.*
376
+ if test -f $item
377
+ set -a items $item
378
+ end
379
+ end
380
+ end
381
+ end
292
382
  case directory
293
383
  # Complete directories only
294
384
  for item in $current*
@@ -296,6 +386,15 @@ ${escapedArgs ? ` set -l output (${programName} ${escapedArgs} $prev $current
296
386
  set -a items $item/
297
387
  end
298
388
  end
389
+ if test "$hidden" = "1"
390
+ if test -z "$current"; or string match -q '*/' -- "$current"
391
+ for item in $current.*
392
+ if test -d $item
393
+ set -a items $item/
394
+ end
395
+ end
396
+ end
397
+ end
299
398
  case any
300
399
  # Complete both files and directories
301
400
  for item in $current*
@@ -305,6 +404,17 @@ ${escapedArgs ? ` set -l output (${programName} ${escapedArgs} $prev $current
305
404
  set -a items $item
306
405
  end
307
406
  end
407
+ if test "$hidden" = "1"
408
+ if test -z "$current"; or string match -q '*/' -- "$current"
409
+ for item in $current.*
410
+ if test -d $item
411
+ set -a items $item/
412
+ else if test -f $item
413
+ set -a items $item
414
+ end
415
+ end
416
+ end
417
+ end
308
418
  end
309
419
 
310
420
  # Filter by extensions if specified
@@ -372,7 +482,8 @@ complete -c ${programName} -f -a '(${functionName})'
372
482
  const extensions = suggestion.extensions?.join(",") || "";
373
483
  const hidden = suggestion.includeHidden ? "1" : "0";
374
484
  const description = suggestion.description == null ? "" : formatMessage(suggestion.description, { colors: false });
375
- yield `__FILE__:${suggestion.type}:${extensions}:${suggestion.pattern || ""}:${hidden}\t${description}`;
485
+ const pattern = encodePattern(suggestion.pattern ?? "");
486
+ yield `__FILE__:${suggestion.type}:${extensions}:${pattern}:${hidden}\t${description}`;
376
487
  }
377
488
  i++;
378
489
  }
@@ -478,10 +589,11 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
478
589
  $output | lines | each {|line|
479
590
  if ($line | str starts-with '__FILE__:') {
480
591
  # Parse file completion directive: __FILE__:type:extensions:pattern:hidden
481
- let parts = ($line | split row ':')
592
+ let directive = ($line | split row "\t" | first)
593
+ let parts = ($directive | split row ':')
482
594
  let type = ($parts | get 1)
483
595
  let extensions = ($parts | get 2)
484
- let pattern = ($parts | get 3)
596
+ let pattern = ($parts | get 3 | str replace -a '%3A' ':' | str replace -a '%25' '%')
485
597
  let hidden = ($parts | get 4) == '1'
486
598
 
487
599
  # Extract prefix from the last argument if it exists
@@ -493,31 +605,33 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
493
605
 
494
606
  # Generate file completions based on type
495
607
  # Use current directory if prefix is empty
496
- let ls_pattern = if ($prefix | is-empty) { "." } else { $prefix + "*" }
608
+ # Note: into glob is required so that ls expands wildcards from a variable
609
+ let ls_pattern = if ($prefix | is-empty) { "." } else { ($prefix + "*" | into glob) }
497
610
 
611
+ # Use ls -a to include hidden files when requested
498
612
  let items = try {
499
613
  match $type {
500
614
  "file" => {
501
615
  if ($extensions | is-empty) {
502
- ls $ls_pattern | where type == file
616
+ (if $hidden { ls -a $ls_pattern } else { ls $ls_pattern }) | where type == file
503
617
  } else {
504
618
  let ext_list = ($extensions | split row ',')
505
- ls $ls_pattern | where type == file | where {|f|
619
+ (if $hidden { ls -a $ls_pattern } else { ls $ls_pattern }) | where type == file | where {|f|
506
620
  let ext = ($f.name | path parse | get extension)
507
621
  $ext in $ext_list
508
622
  }
509
623
  }
510
624
  },
511
625
  "directory" => {
512
- ls $ls_pattern | where type == dir
626
+ (if $hidden { ls -a $ls_pattern } else { ls $ls_pattern }) | where type == dir
513
627
  },
514
628
  "any" => {
515
629
  if ($extensions | is-empty) {
516
- ls $ls_pattern
630
+ if $hidden { ls -a $ls_pattern } else { ls $ls_pattern }
517
631
  } else {
518
632
  let ext_list = ($extensions | split row ',')
519
- let dirs = ls $ls_pattern | where type == dir
520
- let files = ls $ls_pattern | where type == file | where {|f|
633
+ let dirs = (if $hidden { ls -a $ls_pattern } else { ls $ls_pattern }) | where type == dir
634
+ let files = (if $hidden { ls -a $ls_pattern } else { ls $ls_pattern }) | where type == file | where {|f|
521
635
  let ext = ($f.name | path parse | get extension)
522
636
  $ext in $ext_list
523
637
  }
@@ -539,12 +653,22 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
539
653
  }
540
654
  }
541
655
 
656
+ # Extract directory prefix to preserve in completion text
657
+ let dir_prefix = if ($prefix | is-empty) {
658
+ ""
659
+ } else if ($prefix | str ends-with "/") {
660
+ $prefix
661
+ } else {
662
+ let parsed = ($prefix | path parse)
663
+ if ($parsed.parent | is-empty) { "" } else if ($parsed.parent | str ends-with "/") { $parsed.parent } else { $parsed.parent + "/" }
664
+ }
665
+
542
666
  # Format file completions
543
667
  $filtered | each {|item|
544
668
  let name = if $item.type == dir {
545
- ($item.name | path basename) + "/"
669
+ $dir_prefix + ($item.name | path basename) + "/"
546
670
  } else {
547
- $item.name | path basename
671
+ $dir_prefix + ($item.name | path basename)
548
672
  }
549
673
  { value: $name }
550
674
  }
@@ -602,7 +726,8 @@ ${functionName}-external
602
726
  const extensions = suggestion.extensions?.join(",") || "";
603
727
  const hidden = suggestion.includeHidden ? "1" : "0";
604
728
  const description = suggestion.description == null ? "" : formatMessage(suggestion.description, { colors: false });
605
- yield `__FILE__:${suggestion.type}:${extensions}:${suggestion.pattern || ""}:${hidden}\t${description}`;
729
+ const pattern = encodePattern(suggestion.pattern ?? "");
730
+ yield `__FILE__:${suggestion.type}:${extensions}:${pattern}:${hidden}\t${description}`;
606
731
  }
607
732
  i++;
608
733
  }
@@ -668,15 +793,19 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
668
793
 
669
794
  if (\$line -match '^__FILE__:') {
670
795
  # Parse file completion directive: __FILE__:type:extensions:pattern:hidden
671
- \$parts = \$line -split ':', 5
796
+ \$directive = (\$line -split "\`t")[0]
797
+ \$parts = \$directive -split ':', 5
672
798
  \$type = \$parts[1]
673
799
  \$extensions = \$parts[2]
674
- \$pattern = \$parts[3]
800
+ \$pattern = \$parts[3] -replace '%3A', ':' -replace '%25', '%'
675
801
  \$hidden = \$parts[4] -eq '1'
676
802
 
677
803
  # Determine current prefix for file matching
678
804
  \$prefix = if (\$wordToComplete) { \$wordToComplete } else { '' }
679
805
 
806
+ # Use -Force to include hidden files when requested
807
+ \$forceParam = if (\$hidden) { @{Force = \$true} } else { @{} }
808
+
680
809
  # Get file system items based on type
681
810
  \$items = @()
682
811
  switch (\$type) {
@@ -684,31 +813,31 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
684
813
  if (\$extensions) {
685
814
  # Filter by extensions
686
815
  \$extList = \$extensions -split ','
687
- \$items = Get-ChildItem -File -Path "\${prefix}*" -ErrorAction SilentlyContinue |
816
+ \$items = Get-ChildItem @forceParam -File -Path "\${prefix}*" -ErrorAction SilentlyContinue |
688
817
  Where-Object {
689
818
  \$ext = \$_.Extension
690
819
  \$extList | ForEach-Object { if (\$ext -eq ".\$_") { return \$true } }
691
820
  }
692
821
  } else {
693
- \$items = Get-ChildItem -File -Path "\${prefix}*" -ErrorAction SilentlyContinue
822
+ \$items = Get-ChildItem @forceParam -File -Path "\${prefix}*" -ErrorAction SilentlyContinue
694
823
  }
695
824
  }
696
825
  'directory' {
697
- \$items = Get-ChildItem -Directory -Path "\${prefix}*" -ErrorAction SilentlyContinue
826
+ \$items = Get-ChildItem @forceParam -Directory -Path "\${prefix}*" -ErrorAction SilentlyContinue
698
827
  }
699
828
  'any' {
700
829
  if (\$extensions) {
701
830
  # Get directories and filtered files
702
- \$dirs = Get-ChildItem -Directory -Path "\${prefix}*" -ErrorAction SilentlyContinue
831
+ \$dirs = Get-ChildItem @forceParam -Directory -Path "\${prefix}*" -ErrorAction SilentlyContinue
703
832
  \$extList = \$extensions -split ','
704
- \$files = Get-ChildItem -File -Path "\${prefix}*" -ErrorAction SilentlyContinue |
833
+ \$files = Get-ChildItem @forceParam -File -Path "\${prefix}*" -ErrorAction SilentlyContinue |
705
834
  Where-Object {
706
835
  \$ext = \$_.Extension
707
836
  \$extList | ForEach-Object { if (\$ext -eq ".\$_") { return \$true } }
708
837
  }
709
838
  \$items = \$dirs + \$files
710
839
  } else {
711
- \$items = Get-ChildItem -Path "\${prefix}*" -ErrorAction SilentlyContinue
840
+ \$items = Get-ChildItem @forceParam -Path "\${prefix}*" -ErrorAction SilentlyContinue
712
841
  }
713
842
  }
714
843
  }
@@ -718,9 +847,15 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
718
847
  \$items = \$items | Where-Object { -not \$_.Attributes.HasFlag([System.IO.FileAttributes]::Hidden) }
719
848
  }
720
849
 
850
+ # Extract directory prefix to preserve in completion text
851
+ \$dirPrefix = if (\$prefix -and (\$prefix.Contains('/') -or \$prefix.Contains('\\'))) {
852
+ \$slashIdx = [Math]::Max(\$prefix.LastIndexOf('/'), \$prefix.LastIndexOf('\\'))
853
+ \$prefix.Substring(0, \$slashIdx + 1)
854
+ } else { '' }
855
+
721
856
  # Create completion results for files
722
857
  \$items | ForEach-Object {
723
- \$completionText = if (\$_.PSIsContainer) { "\$(\$_.Name)/" } else { \$_.Name }
858
+ \$completionText = if (\$_.PSIsContainer) { "\$dirPrefix\$(\$_.Name)/" } else { "\$dirPrefix\$(\$_.Name)" }
724
859
  \$itemType = if (\$_.PSIsContainer) { 'Directory' } else { 'File' }
725
860
  [System.Management.Automation.CompletionResult]::new(
726
861
  \$completionText,
@@ -761,7 +896,8 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
761
896
  const extensions = suggestion.extensions?.join(",") || "";
762
897
  const hidden = suggestion.includeHidden ? "1" : "0";
763
898
  const description = suggestion.description == null ? "" : formatMessage(suggestion.description, { colors: false });
764
- yield `__FILE__:${suggestion.type}:${extensions}:${suggestion.pattern || ""}:${hidden}\t[file]\t${description}`;
899
+ const pattern = encodePattern(suggestion.pattern ?? "");
900
+ yield `__FILE__:${suggestion.type}:${extensions}:${pattern}:${hidden}\t[file]\t${description}`;
765
901
  }
766
902
  i++;
767
903
  }