@optique/core 0.10.7 → 1.0.0-dev.1116

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 +194 -52
  7. package/dist/completion.js +194 -52
  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 +391 -106
  53. package/dist/valueparser.d.cts +62 -10
  54. package/dist/valueparser.d.ts +62 -10
  55. package/dist/valueparser.js +391 -106
  56. package/package.json +10 -1
@@ -5,16 +5,17 @@ const require_message = require('./message.cjs');
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,15 +247,22 @@ 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
 
253
+ # Enable glob_dots when hidden files are requested so that
254
+ # _files and _directories include dot-prefixed entries
255
+ local __was_glob_dots=0
256
+ [[ -o glob_dots ]] && __was_glob_dots=1
257
+ if [[ "\$hidden" == "1" ]]; then setopt glob_dots; fi
258
+
180
259
  # Use zsh's native file completion
181
260
  case "\$type" in
182
261
  file)
183
262
  if [[ -n "\$extensions" ]]; then
184
263
  # Complete files with extension filtering
185
- local ext_pattern="*.(\\$\{extensions//,/|\})"
186
- _files -g "\\$ext_pattern"
264
+ local ext_pattern="*.(\$\{extensions//,/|\})"
265
+ _files -g "\$ext_pattern"
187
266
  else
188
267
  _files -g "*"
189
268
  fi
@@ -194,16 +273,16 @@ function _${programName.replace(/[^a-zA-Z0-9]/g, "_")} () {
194
273
  any)
195
274
  if [[ -n "\$extensions" ]]; then
196
275
  # Complete both files and directories, with extension filtering for files
197
- local ext_pattern="*.(\\$\{extensions//,/|\})"
198
- _files -g "\\$ext_pattern" && _directories
276
+ local ext_pattern="*.(\$\{extensions//,/|\})"
277
+ _files -g "\$ext_pattern" && _directories
199
278
  else
200
279
  _files
201
280
  fi
202
281
  ;;
203
282
  esac
204
283
 
205
- # Note: zsh's _files and _directories handle hidden file filtering automatically
206
- # based on the completion context and user settings
284
+ # Restore glob_dots to its previous state
285
+ if [[ "\$__was_glob_dots" == "1" ]]; then setopt glob_dots; else unsetopt glob_dots; fi
207
286
  else
208
287
  # Regular literal completion
209
288
  if [[ -n "\$value" ]]; then
@@ -241,7 +320,8 @@ compdef _${programName.replace(/[^a-zA-Z0-9]/g, "_")} ${programName}
241
320
  const extensions = suggestion.extensions?.join(",") || "";
242
321
  const hidden = suggestion.includeHidden ? "1" : "0";
243
322
  const description = suggestion.description == null ? "" : require_message.formatMessage(suggestion.description, { colors: false });
244
- yield `__FILE__:${suggestion.type}:${extensions}:${suggestion.pattern || ""}:${hidden}\0${description}\0`;
323
+ const pattern = encodePattern(suggestion.pattern ?? "");
324
+ yield `__FILE__:${suggestion.type}:${extensions}:${pattern}:${hidden}\0${description}\0`;
245
325
  }
246
326
  }
247
327
  };
@@ -273,10 +353,11 @@ ${escapedArgs ? ` set -l output (${programName} ${escapedArgs} $prev $current
273
353
  for line in $output
274
354
  if string match -q '__FILE__:*' -- $line
275
355
  # Parse file completion directive: __FILE__:type:extensions:pattern:hidden
276
- set -l parts (string split ':' -- $line)
356
+ set -l directive (string split \\t -- $line)[1]
357
+ set -l parts (string split ':' -- $directive)
277
358
  set -l type $parts[2]
278
359
  set -l extensions $parts[3]
279
- set -l pattern $parts[4]
360
+ set -l pattern (string replace -a '%25' '%' -- (string replace -a '%3A' ':' -- $parts[4]))
280
361
  set -l hidden $parts[5]
281
362
 
282
363
  # Generate file completions based on type
@@ -289,6 +370,21 @@ ${escapedArgs ? ` set -l output (${programName} ${escapedArgs} $prev $current
289
370
  set -a items $item
290
371
  end
291
372
  end
373
+ # Fish's * glob does not match dotfiles; add them
374
+ # explicitly when the basename is empty (i.e., $current
375
+ # is "" or ends with "/"), because only then are * and
376
+ # .* complementary. When a non-empty basename is present
377
+ # (e.g., "foo"), foo* already covers foo.txt, so foo.*
378
+ # would just produce duplicates.
379
+ if test "$hidden" = "1"
380
+ if test -z "$current"; or string match -q '*/' -- "$current"
381
+ for item in $current.*
382
+ if test -f $item
383
+ set -a items $item
384
+ end
385
+ end
386
+ end
387
+ end
292
388
  case directory
293
389
  # Complete directories only
294
390
  for item in $current*
@@ -296,6 +392,15 @@ ${escapedArgs ? ` set -l output (${programName} ${escapedArgs} $prev $current
296
392
  set -a items $item/
297
393
  end
298
394
  end
395
+ if test "$hidden" = "1"
396
+ if test -z "$current"; or string match -q '*/' -- "$current"
397
+ for item in $current.*
398
+ if test -d $item
399
+ set -a items $item/
400
+ end
401
+ end
402
+ end
403
+ end
299
404
  case any
300
405
  # Complete both files and directories
301
406
  for item in $current*
@@ -305,6 +410,17 @@ ${escapedArgs ? ` set -l output (${programName} ${escapedArgs} $prev $current
305
410
  set -a items $item
306
411
  end
307
412
  end
413
+ if test "$hidden" = "1"
414
+ if test -z "$current"; or string match -q '*/' -- "$current"
415
+ for item in $current.*
416
+ if test -d $item
417
+ set -a items $item/
418
+ else if test -f $item
419
+ set -a items $item
420
+ end
421
+ end
422
+ end
423
+ end
308
424
  end
309
425
 
310
426
  # Filter by extensions if specified
@@ -372,7 +488,8 @@ complete -c ${programName} -f -a '(${functionName})'
372
488
  const extensions = suggestion.extensions?.join(",") || "";
373
489
  const hidden = suggestion.includeHidden ? "1" : "0";
374
490
  const description = suggestion.description == null ? "" : require_message.formatMessage(suggestion.description, { colors: false });
375
- yield `__FILE__:${suggestion.type}:${extensions}:${suggestion.pattern || ""}:${hidden}\t${description}`;
491
+ const pattern = encodePattern(suggestion.pattern ?? "");
492
+ yield `__FILE__:${suggestion.type}:${extensions}:${pattern}:${hidden}\t${description}`;
376
493
  }
377
494
  i++;
378
495
  }
@@ -478,10 +595,11 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
478
595
  $output | lines | each {|line|
479
596
  if ($line | str starts-with '__FILE__:') {
480
597
  # Parse file completion directive: __FILE__:type:extensions:pattern:hidden
481
- let parts = ($line | split row ':')
598
+ let directive = ($line | split row "\t" | first)
599
+ let parts = ($directive | split row ':')
482
600
  let type = ($parts | get 1)
483
601
  let extensions = ($parts | get 2)
484
- let pattern = ($parts | get 3)
602
+ let pattern = ($parts | get 3 | str replace -a '%3A' ':' | str replace -a '%25' '%')
485
603
  let hidden = ($parts | get 4) == '1'
486
604
 
487
605
  # Extract prefix from the last argument if it exists
@@ -493,31 +611,33 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
493
611
 
494
612
  # Generate file completions based on type
495
613
  # Use current directory if prefix is empty
496
- let ls_pattern = if ($prefix | is-empty) { "." } else { $prefix + "*" }
614
+ # Note: into glob is required so that ls expands wildcards from a variable
615
+ let ls_pattern = if ($prefix | is-empty) { "." } else { ($prefix + "*" | into glob) }
497
616
 
617
+ # Use ls -a to include hidden files when requested
498
618
  let items = try {
499
619
  match $type {
500
620
  "file" => {
501
621
  if ($extensions | is-empty) {
502
- ls $ls_pattern | where type == file
622
+ (if $hidden { ls -a $ls_pattern } else { ls $ls_pattern }) | where type == file
503
623
  } else {
504
624
  let ext_list = ($extensions | split row ',')
505
- ls $ls_pattern | where type == file | where {|f|
625
+ (if $hidden { ls -a $ls_pattern } else { ls $ls_pattern }) | where type == file | where {|f|
506
626
  let ext = ($f.name | path parse | get extension)
507
627
  $ext in $ext_list
508
628
  }
509
629
  }
510
630
  },
511
631
  "directory" => {
512
- ls $ls_pattern | where type == dir
632
+ (if $hidden { ls -a $ls_pattern } else { ls $ls_pattern }) | where type == dir
513
633
  },
514
634
  "any" => {
515
635
  if ($extensions | is-empty) {
516
- ls $ls_pattern
636
+ if $hidden { ls -a $ls_pattern } else { ls $ls_pattern }
517
637
  } else {
518
638
  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|
639
+ let dirs = (if $hidden { ls -a $ls_pattern } else { ls $ls_pattern }) | where type == dir
640
+ let files = (if $hidden { ls -a $ls_pattern } else { ls $ls_pattern }) | where type == file | where {|f|
521
641
  let ext = ($f.name | path parse | get extension)
522
642
  $ext in $ext_list
523
643
  }
@@ -539,12 +659,22 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
539
659
  }
540
660
  }
541
661
 
662
+ # Extract directory prefix to preserve in completion text
663
+ let dir_prefix = if ($prefix | is-empty) {
664
+ ""
665
+ } else if ($prefix | str ends-with "/") {
666
+ $prefix
667
+ } else {
668
+ let parsed = ($prefix | path parse)
669
+ if ($parsed.parent | is-empty) { "" } else if ($parsed.parent | str ends-with "/") { $parsed.parent } else { $parsed.parent + "/" }
670
+ }
671
+
542
672
  # Format file completions
543
673
  $filtered | each {|item|
544
674
  let name = if $item.type == dir {
545
- ($item.name | path basename) + "/"
675
+ $dir_prefix + ($item.name | path basename) + "/"
546
676
  } else {
547
- $item.name | path basename
677
+ $dir_prefix + ($item.name | path basename)
548
678
  }
549
679
  { value: $name }
550
680
  }
@@ -602,7 +732,8 @@ ${functionName}-external
602
732
  const extensions = suggestion.extensions?.join(",") || "";
603
733
  const hidden = suggestion.includeHidden ? "1" : "0";
604
734
  const description = suggestion.description == null ? "" : require_message.formatMessage(suggestion.description, { colors: false });
605
- yield `__FILE__:${suggestion.type}:${extensions}:${suggestion.pattern || ""}:${hidden}\t${description}`;
735
+ const pattern = encodePattern(suggestion.pattern ?? "");
736
+ yield `__FILE__:${suggestion.type}:${extensions}:${pattern}:${hidden}\t${description}`;
606
737
  }
607
738
  i++;
608
739
  }
@@ -668,15 +799,19 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
668
799
 
669
800
  if (\$line -match '^__FILE__:') {
670
801
  # Parse file completion directive: __FILE__:type:extensions:pattern:hidden
671
- \$parts = \$line -split ':', 5
802
+ \$directive = (\$line -split "\`t")[0]
803
+ \$parts = \$directive -split ':', 5
672
804
  \$type = \$parts[1]
673
805
  \$extensions = \$parts[2]
674
- \$pattern = \$parts[3]
806
+ \$pattern = \$parts[3] -replace '%3A', ':' -replace '%25', '%'
675
807
  \$hidden = \$parts[4] -eq '1'
676
808
 
677
809
  # Determine current prefix for file matching
678
810
  \$prefix = if (\$wordToComplete) { \$wordToComplete } else { '' }
679
811
 
812
+ # Use -Force to include hidden files when requested
813
+ \$forceParam = if (\$hidden) { @{Force = \$true} } else { @{} }
814
+
680
815
  # Get file system items based on type
681
816
  \$items = @()
682
817
  switch (\$type) {
@@ -684,31 +819,31 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
684
819
  if (\$extensions) {
685
820
  # Filter by extensions
686
821
  \$extList = \$extensions -split ','
687
- \$items = Get-ChildItem -File -Path "\${prefix}*" -ErrorAction SilentlyContinue |
822
+ \$items = Get-ChildItem @forceParam -File -Path "\${prefix}*" -ErrorAction SilentlyContinue |
688
823
  Where-Object {
689
824
  \$ext = \$_.Extension
690
825
  \$extList | ForEach-Object { if (\$ext -eq ".\$_") { return \$true } }
691
826
  }
692
827
  } else {
693
- \$items = Get-ChildItem -File -Path "\${prefix}*" -ErrorAction SilentlyContinue
828
+ \$items = Get-ChildItem @forceParam -File -Path "\${prefix}*" -ErrorAction SilentlyContinue
694
829
  }
695
830
  }
696
831
  'directory' {
697
- \$items = Get-ChildItem -Directory -Path "\${prefix}*" -ErrorAction SilentlyContinue
832
+ \$items = Get-ChildItem @forceParam -Directory -Path "\${prefix}*" -ErrorAction SilentlyContinue
698
833
  }
699
834
  'any' {
700
835
  if (\$extensions) {
701
836
  # Get directories and filtered files
702
- \$dirs = Get-ChildItem -Directory -Path "\${prefix}*" -ErrorAction SilentlyContinue
837
+ \$dirs = Get-ChildItem @forceParam -Directory -Path "\${prefix}*" -ErrorAction SilentlyContinue
703
838
  \$extList = \$extensions -split ','
704
- \$files = Get-ChildItem -File -Path "\${prefix}*" -ErrorAction SilentlyContinue |
839
+ \$files = Get-ChildItem @forceParam -File -Path "\${prefix}*" -ErrorAction SilentlyContinue |
705
840
  Where-Object {
706
841
  \$ext = \$_.Extension
707
842
  \$extList | ForEach-Object { if (\$ext -eq ".\$_") { return \$true } }
708
843
  }
709
844
  \$items = \$dirs + \$files
710
845
  } else {
711
- \$items = Get-ChildItem -Path "\${prefix}*" -ErrorAction SilentlyContinue
846
+ \$items = Get-ChildItem @forceParam -Path "\${prefix}*" -ErrorAction SilentlyContinue
712
847
  }
713
848
  }
714
849
  }
@@ -718,9 +853,15 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
718
853
  \$items = \$items | Where-Object { -not \$_.Attributes.HasFlag([System.IO.FileAttributes]::Hidden) }
719
854
  }
720
855
 
856
+ # Extract directory prefix to preserve in completion text
857
+ \$dirPrefix = if (\$prefix -and (\$prefix.Contains('/') -or \$prefix.Contains('\\'))) {
858
+ \$slashIdx = [Math]::Max(\$prefix.LastIndexOf('/'), \$prefix.LastIndexOf('\\'))
859
+ \$prefix.Substring(0, \$slashIdx + 1)
860
+ } else { '' }
861
+
721
862
  # Create completion results for files
722
863
  \$items | ForEach-Object {
723
- \$completionText = if (\$_.PSIsContainer) { "\$(\$_.Name)/" } else { \$_.Name }
864
+ \$completionText = if (\$_.PSIsContainer) { "\$dirPrefix\$(\$_.Name)/" } else { "\$dirPrefix\$(\$_.Name)" }
724
865
  \$itemType = if (\$_.PSIsContainer) { 'Directory' } else { 'File' }
725
866
  [System.Management.Automation.CompletionResult]::new(
726
867
  \$completionText,
@@ -761,7 +902,8 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
761
902
  const extensions = suggestion.extensions?.join(",") || "";
762
903
  const hidden = suggestion.includeHidden ? "1" : "0";
763
904
  const description = suggestion.description == null ? "" : require_message.formatMessage(suggestion.description, { colors: false });
764
- yield `__FILE__:${suggestion.type}:${extensions}:${suggestion.pattern || ""}:${hidden}\t[file]\t${description}`;
905
+ const pattern = encodePattern(suggestion.pattern ?? "");
906
+ yield `__FILE__:${suggestion.type}:${extensions}:${pattern}:${hidden}\t[file]\t${description}`;
765
907
  }
766
908
  i++;
767
909
  }