@optique/core 1.1.11 → 1.1.13-dev.2823

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.
@@ -40,6 +40,12 @@ function validateProgramName(programName) {
40
40
  function encodePattern(pattern) {
41
41
  return pattern.replace(/%/g, "%25").replace(/:/g, "%3A");
42
42
  }
43
+ /** Encodes optional literal insertion metadata without changing old directives. */
44
+ function encodeFilePrefix(suggestion) {
45
+ const prefix = suggestion.prefix ?? "";
46
+ if (prefix === "" || !(suggestion.pattern ?? "").startsWith(prefix) || sanitizeForTransport(prefix) !== prefix) return "";
47
+ return `:${encodePattern(prefix)}`;
48
+ }
43
49
  function encodeExtensions(extensions) {
44
50
  return extensions?.map((ext) => ext.replace(/^\./, "")).join(",") ?? "";
45
51
  }
@@ -69,13 +75,32 @@ function _${programName} () {
69
75
  COMPREPLY=()
70
76
  local current="\${COMP_WORDS[COMP_CWORD]}"
71
77
  local prev=("\${COMP_WORDS[@]:1:COMP_CWORD-1}")
78
+ local __prefix_aware=0
72
79
  while IFS= read -r line; do
73
80
  if [[ "$line" == __FILE__:* ]]; then
74
81
  # Parse file completion directive: __FILE__:type:extensions:pattern:hidden
75
- IFS=':' read -r _ type extensions pattern hidden <<< "$line"
82
+ local insertion_prefix
83
+ IFS=':' read -r _ type extensions pattern hidden insertion_prefix <<< "$line"
76
84
  extensions="\${extensions//,\\./,}"; extensions="\${extensions#.}"
77
85
  pattern="\${pattern//%3A/:}"; pattern="\${pattern//%25/%}"
78
86
 
87
+ insertion_prefix="\${insertion_prefix//%3A/:}"; insertion_prefix="\${insertion_prefix//%25/%}"
88
+ local __saved_current="$current"
89
+ local -a __saved_reply=()
90
+ if [[ -n "$insertion_prefix" && "$pattern" == "$insertion_prefix"* && "$current" == "$insertion_prefix"* ]]; then
91
+ pattern="\${pattern#"$insertion_prefix"}"
92
+ current="\${current#"$insertion_prefix"}"
93
+ __prefix_aware=1
94
+ else
95
+ insertion_prefix=""
96
+ fi
97
+ # Inserted keys are not filesystem paths. Once present, keep earlier
98
+ # replies out of later file filters; retain legacy-only aggregation.
99
+ if [[ "$__prefix_aware" == "1" ]]; then
100
+ __saved_reply=("\${COMPREPLY[@]}")
101
+ COMPREPLY=()
102
+ fi
103
+
79
104
  # Save and adjust glob/shell options for safe file completion
80
105
  local __dotglob_was_set=0 __failglob_was_set=0 __noglob_was_set=0
81
106
  local __globignore_was_set=0 __saved_globignore="\${GLOBIGNORE-}"
@@ -274,6 +299,16 @@ function _${programName} () {
274
299
  done
275
300
  COMPREPLY=("\${filtered[@]}")
276
301
  fi
302
+ if [[ -n "$insertion_prefix" ]]; then
303
+ local __prefix_index
304
+ for __prefix_index in "\${!COMPREPLY[@]}"; do
305
+ COMPREPLY[$__prefix_index]="$insertion_prefix\${COMPREPLY[$__prefix_index]}"
306
+ done
307
+ fi
308
+ if [[ "$__prefix_aware" == "1" ]]; then
309
+ COMPREPLY=("\${__saved_reply[@]}" "\${COMPREPLY[@]}")
310
+ fi
311
+ current="$__saved_current"
277
312
  else
278
313
  # Regular literal completion
279
314
  COMPREPLY+=("$line")
@@ -293,7 +328,7 @@ complete -F _${programName} -- ${programName}
293
328
  const extensions = encodeExtensions(suggestion.extensions);
294
329
  const hidden = suggestion.includeHidden ? "1" : "0";
295
330
  const pattern = encodePattern(suggestion.pattern ?? "");
296
- yield `__FILE__:${suggestion.type}:${extensions}:${pattern}:${hidden}`;
331
+ yield `__FILE__:${suggestion.type}:${extensions}:${pattern}:${hidden}${encodeFilePrefix(suggestion)}`;
297
332
  }
298
333
  i++;
299
334
  }
@@ -343,9 +378,20 @@ function _${programName.replace(/[^a-zA-Z0-9]/g, "_")} () {
343
378
 
344
379
  if [[ "\$value" == __FILE__:* ]]; then
345
380
  # Parse file completion directive: __FILE__:type:extensions:pattern:hidden
346
- local type extensions pattern hidden
347
- IFS=':' read -r _ type extensions pattern hidden <<< "\$value"
348
- pattern="\${pattern//%3A/:}"; pattern="\${pattern//%25/%}"
381
+ local type extensions pattern hidden insertion_prefix
382
+ IFS=':' read -r _ type extensions pattern hidden insertion_prefix <<< "\$value"
383
+ local __decoded_pattern="\$pattern"
384
+ __decoded_pattern="\${__decoded_pattern//\\%3A/:}"; __decoded_pattern="\${__decoded_pattern//\\%25/%}"
385
+ insertion_prefix="\${insertion_prefix//\\%3A/:}"; insertion_prefix="\${insertion_prefix//\\%25/%}"
386
+ local __saved_prefix="\$PREFIX" __saved_iprefix="\$IPREFIX"
387
+ if [[ -n "\$insertion_prefix" && "\$__decoded_pattern" == "\$insertion_prefix"* && "\$PREFIX" == "\$insertion_prefix"* ]]; then
388
+ pattern="\${__decoded_pattern#"\$insertion_prefix"}"
389
+ PREFIX="\${PREFIX#"\$insertion_prefix"}"
390
+ IPREFIX="\$IPREFIX\$insertion_prefix"
391
+ else
392
+ # Preserve legacy decoding for directives without active metadata.
393
+ pattern="\${pattern//%3A/:}"; pattern="\${pattern//%25/%}"
394
+ fi
349
395
  has_file_completion=1
350
396
 
351
397
  # Native completion helpers expect normal zsh glob grouping even if
@@ -364,7 +410,6 @@ function _${programName.replace(/[^a-zA-Z0-9]/g, "_")} () {
364
410
  # _directories enumerate the pattern's directory instead of the
365
411
  # current word. If the user has already typed beyond the pattern,
366
412
  # keep PREFIX unchanged for incremental narrowing.
367
- local __saved_prefix="\$PREFIX"
368
413
  if [[ -n "\$pattern" ]]; then
369
414
  # Normalize leading ./ so that ./src/ma matches pattern src/
370
415
  local __norm_prefix="\${PREFIX#./}"
@@ -421,6 +466,7 @@ function _${programName.replace(/[^a-zA-Z0-9]/g, "_")} () {
421
466
 
422
467
  # Restore PREFIX and glob_dots to their previous state
423
468
  PREFIX="\$__saved_prefix"
469
+ IPREFIX="\$__saved_iprefix"
424
470
  if [[ "\$__was_sh_glob" == "1" ]]; then setopt sh_glob; else unsetopt sh_glob; fi
425
471
  if [[ "\$__was_glob_dots" == "1" ]]; then setopt glob_dots; else unsetopt glob_dots; fi
426
472
  else
@@ -463,7 +509,7 @@ fi
463
509
  const hidden = suggestion.includeHidden ? "1" : "0";
464
510
  const description = suggestion.description == null ? "" : sanitizeForTransport(require_message.formatMessage(suggestion.description, { colors: false }));
465
511
  const pattern = encodePattern(suggestion.pattern ?? "");
466
- yield `__FILE__:${suggestion.type}:${extensions}:${pattern}:${hidden}\0${description}\0`;
512
+ yield `__FILE__:${suggestion.type}:${extensions}:${pattern}:${hidden}${encodeFilePrefix(suggestion)}\0${description}\0`;
467
513
  }
468
514
  }
469
515
  };
@@ -501,6 +547,21 @@ ${escapedArgs ? ` set -l output (${programName} ${escapedArgs} $prev $current
501
547
  set -l extensions $parts[3]
502
548
  set -l pattern (string replace -a '%25' '%' -- (string replace -a '%3A' ':' -- $parts[4]))
503
549
  set -l hidden $parts[5]
550
+ set -l insertion_prefix ""
551
+ if test (count $parts) -ge 6
552
+ set insertion_prefix (string replace -a '%25' '%' -- (string replace -a '%3A' ':' -- $parts[6]))
553
+ end
554
+ set -l saved_current "$current"
555
+ set -l prefix_length (string length -- "$insertion_prefix")
556
+ if test $prefix_length -gt 0
557
+ and test (string sub -l $prefix_length -- "$pattern") = "$insertion_prefix"
558
+ and test (string sub -l $prefix_length -- "$current") = "$insertion_prefix"
559
+ set -l start (math $prefix_length + 1)
560
+ set pattern (string sub -s $start -- "$pattern")
561
+ set current (string sub -s $start -- "$current")
562
+ else
563
+ set insertion_prefix ""
564
+ end
504
565
 
505
566
  # When a pattern is specified, use it as the glob base instead
506
567
  # of the current word. If the user has already typed beyond the
@@ -722,8 +783,9 @@ ${escapedArgs ? ` set -l output (${programName} ${escapedArgs} $prev $current
722
783
 
723
784
  # Output file completions
724
785
  for item in $items
725
- echo $item
786
+ echo "$insertion_prefix$item"
726
787
  end
788
+ set current "$saved_current"
727
789
  else
728
790
  # Regular literal completion - split by tab
729
791
  set -l parts (string split \\t -- $line)
@@ -753,7 +815,7 @@ complete -c ${programName} -f -a '(${functionName})'
753
815
  const hidden = suggestion.includeHidden ? "1" : "0";
754
816
  const description = suggestion.description == null ? "" : sanitizeForTransport(require_message.formatMessage(suggestion.description, { colors: false }));
755
817
  const pattern = encodePattern(suggestion.pattern ?? "");
756
- yield `__FILE__:${suggestion.type}:${extensions}:${pattern}:${hidden}\t${description}`;
818
+ yield `__FILE__:${suggestion.type}:${extensions}:${pattern}:${hidden}${encodeFilePrefix(suggestion)}\t${description}`;
757
819
  }
758
820
  i++;
759
821
  }
@@ -866,6 +928,10 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
866
928
  let pattern = ($parts | get 3 | str replace -a '%3A' ':' | str replace -a '%25' '%')
867
929
  let hidden = ($parts | get 4) == '1'
868
930
 
931
+ let insertion_prefix = if ($parts | length) > 5 {
932
+ $parts | get 5 | str replace -a '%3A' ':' | str replace -a '%25' '%'
933
+ } else { "" }
934
+
869
935
  # Extract prefix from the last argument if it exists
870
936
  let prefix = if ($final_args | length) > 0 {
871
937
  $final_args | last
@@ -873,6 +939,11 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
873
939
  ""
874
940
  }
875
941
 
942
+ let insertion_prefix = if ($insertion_prefix | is-not-empty) and ($pattern | str starts-with $insertion_prefix) and ($prefix | str starts-with $insertion_prefix) { $insertion_prefix } else { "" }
943
+ let prefix_length = ($insertion_prefix | str length)
944
+ let pattern = if $prefix_length > 0 { $pattern | str substring $prefix_length.. } else { $pattern }
945
+ let prefix = if $prefix_length > 0 { $prefix | str substring $prefix_length.. } else { $prefix }
946
+
876
947
  # When a pattern is specified, use it as the glob base instead of
877
948
  # the user-typed prefix. If the user has already typed beyond the
878
949
  # pattern (e.g., pattern="src/" and prefix="src/ma"), keep the
@@ -1018,7 +1089,7 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
1018
1089
  } else {
1019
1090
  $dir_prefix + ($item.name | path basename)
1020
1091
  }
1021
- { value: $name }
1092
+ { value: ($insertion_prefix + $name) }
1022
1093
  }
1023
1094
  } else {
1024
1095
  # Regular literal completion - split by tab
@@ -1075,7 +1146,7 @@ ${functionName}-external
1075
1146
  const hidden = suggestion.includeHidden ? "1" : "0";
1076
1147
  const description = suggestion.description == null ? "" : sanitizeForTransport(require_message.formatMessage(suggestion.description, { colors: false }));
1077
1148
  const pattern = encodePattern(suggestion.pattern ?? "");
1078
- yield `__FILE__:${suggestion.type}:${extensions}:${pattern}:${hidden}\t${description}`;
1149
+ yield `__FILE__:${suggestion.type}:${extensions}:${pattern}:${hidden}${encodeFilePrefix(suggestion)}\t${description}`;
1079
1150
  }
1080
1151
  i++;
1081
1152
  }
@@ -1142,11 +1213,17 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
1142
1213
  if (\$line -match '^__FILE__:') {
1143
1214
  # Parse file completion directive: __FILE__:type:extensions:pattern:hidden
1144
1215
  \$directive = (\$line -split "\`t")[0]
1145
- \$parts = \$directive -split ':', 5
1216
+ \$parts = \$directive -split ':', 6
1146
1217
  \$type = \$parts[1]
1147
1218
  \$extensions = \$parts[2]
1148
1219
  \$pattern = \$parts[3] -replace '%3A', ':' -replace '%25', '%'
1149
1220
  \$hidden = \$parts[4] -eq '1'
1221
+ \$insertionPrefix = if (\$parts.Length -gt 5) { \$parts[5] -replace '%3A', ':' -replace '%25', '%' } else { '' }
1222
+ \$matchingWord = \$wordToComplete
1223
+ if (\$insertionPrefix -and \$pattern.StartsWith(\$insertionPrefix, [System.StringComparison]::Ordinal) -and \$matchingWord.StartsWith(\$insertionPrefix, [System.StringComparison]::Ordinal)) {
1224
+ \$pattern = \$pattern.Substring(\$insertionPrefix.Length)
1225
+ \$matchingWord = \$matchingWord.Substring(\$insertionPrefix.Length)
1226
+ } else { \$insertionPrefix = '' }
1150
1227
 
1151
1228
  # When a pattern is specified, use it as the file matching
1152
1229
  # prefix instead of the current word. If the user has
@@ -1155,7 +1232,7 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
1155
1232
  # comparing so that Windows backslashes match forward slashes
1156
1233
  # in the transported pattern.
1157
1234
  \$normalizedPattern = if (\$pattern) { \$pattern.Replace('\\', '/') -replace '^\\./','' } else { '' }
1158
- \$normalizedWord = if (\$wordToComplete) { \$wordToComplete.Replace('\\', '/') -replace '^\\./','' } else { '' }
1235
+ \$normalizedWord = if (\$matchingWord) { \$matchingWord.Replace('\\', '/') -replace '^\\./','' } else { '' }
1159
1236
  \$comparison = if (\$IsWindows) { [System.StringComparison]::OrdinalIgnoreCase } else { [System.StringComparison]::Ordinal }
1160
1237
  # For wildcard patterns, compare only the directory prefix
1161
1238
  \$comparePattern = \$normalizedPattern
@@ -1167,12 +1244,12 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
1167
1244
  \$comparePattern = ''
1168
1245
  }
1169
1246
  }
1170
- \$prefix = if ((\$comparePattern -or \$normalizedWord) -and \$normalizedWord -and \$normalizedWord.StartsWith(\$comparePattern, \$comparison) -and \$normalizedWord.Length -ge \$comparePattern.Length -and \$wordToComplete -ne \$pattern) {
1171
- \$wordToComplete
1247
+ \$prefix = if ((\$comparePattern -or \$normalizedWord) -and \$normalizedWord -and \$normalizedWord.StartsWith(\$comparePattern, \$comparison) -and \$normalizedWord.Length -ge \$comparePattern.Length -and \$matchingWord -ne \$pattern) {
1248
+ \$matchingWord
1172
1249
  } elseif (\$pattern) {
1173
1250
  \$pattern
1174
- } elseif (\$wordToComplete) {
1175
- \$wordToComplete
1251
+ } elseif (\$matchingWord) {
1252
+ \$matchingWord
1176
1253
  } else { '' }
1177
1254
 
1178
1255
  # Use -Force to include hidden files when requested, or when
@@ -1247,7 +1324,7 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
1247
1324
  \$completionText = if (\$_.PSIsContainer) { "\$dirPrefix\$(\$_.Name)/" } else { "\$dirPrefix\$(\$_.Name)" }
1248
1325
  \$itemType = if (\$_.PSIsContainer) { 'Directory' } else { 'File' }
1249
1326
  [System.Management.Automation.CompletionResult]::new(
1250
- \$completionText,
1327
+ (\$insertionPrefix + \$completionText),
1251
1328
  \$completionText,
1252
1329
  'ParameterValue',
1253
1330
  \$itemType
@@ -1287,7 +1364,7 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
1287
1364
  const hidden = suggestion.includeHidden ? "1" : "0";
1288
1365
  const description = suggestion.description == null ? "" : sanitizeForTransport(require_message.formatMessage(suggestion.description, { colors: false }));
1289
1366
  const pattern = encodePattern(suggestion.pattern ?? "");
1290
- yield `__FILE__:${suggestion.type}:${extensions}:${pattern}:${hidden}\t[file]\t${description}`;
1367
+ yield `__FILE__:${suggestion.type}:${extensions}:${pattern}:${hidden}${encodeFilePrefix(suggestion)}\t[file]\t${description}`;
1291
1368
  }
1292
1369
  i++;
1293
1370
  }
@@ -40,6 +40,12 @@ function validateProgramName(programName) {
40
40
  function encodePattern(pattern) {
41
41
  return pattern.replace(/%/g, "%25").replace(/:/g, "%3A");
42
42
  }
43
+ /** Encodes optional literal insertion metadata without changing old directives. */
44
+ function encodeFilePrefix(suggestion) {
45
+ const prefix = suggestion.prefix ?? "";
46
+ if (prefix === "" || !(suggestion.pattern ?? "").startsWith(prefix) || sanitizeForTransport(prefix) !== prefix) return "";
47
+ return `:${encodePattern(prefix)}`;
48
+ }
43
49
  function encodeExtensions(extensions) {
44
50
  return extensions?.map((ext) => ext.replace(/^\./, "")).join(",") ?? "";
45
51
  }
@@ -69,13 +75,32 @@ function _${programName} () {
69
75
  COMPREPLY=()
70
76
  local current="\${COMP_WORDS[COMP_CWORD]}"
71
77
  local prev=("\${COMP_WORDS[@]:1:COMP_CWORD-1}")
78
+ local __prefix_aware=0
72
79
  while IFS= read -r line; do
73
80
  if [[ "$line" == __FILE__:* ]]; then
74
81
  # Parse file completion directive: __FILE__:type:extensions:pattern:hidden
75
- IFS=':' read -r _ type extensions pattern hidden <<< "$line"
82
+ local insertion_prefix
83
+ IFS=':' read -r _ type extensions pattern hidden insertion_prefix <<< "$line"
76
84
  extensions="\${extensions//,\\./,}"; extensions="\${extensions#.}"
77
85
  pattern="\${pattern//%3A/:}"; pattern="\${pattern//%25/%}"
78
86
 
87
+ insertion_prefix="\${insertion_prefix//%3A/:}"; insertion_prefix="\${insertion_prefix//%25/%}"
88
+ local __saved_current="$current"
89
+ local -a __saved_reply=()
90
+ if [[ -n "$insertion_prefix" && "$pattern" == "$insertion_prefix"* && "$current" == "$insertion_prefix"* ]]; then
91
+ pattern="\${pattern#"$insertion_prefix"}"
92
+ current="\${current#"$insertion_prefix"}"
93
+ __prefix_aware=1
94
+ else
95
+ insertion_prefix=""
96
+ fi
97
+ # Inserted keys are not filesystem paths. Once present, keep earlier
98
+ # replies out of later file filters; retain legacy-only aggregation.
99
+ if [[ "$__prefix_aware" == "1" ]]; then
100
+ __saved_reply=("\${COMPREPLY[@]}")
101
+ COMPREPLY=()
102
+ fi
103
+
79
104
  # Save and adjust glob/shell options for safe file completion
80
105
  local __dotglob_was_set=0 __failglob_was_set=0 __noglob_was_set=0
81
106
  local __globignore_was_set=0 __saved_globignore="\${GLOBIGNORE-}"
@@ -274,6 +299,16 @@ function _${programName} () {
274
299
  done
275
300
  COMPREPLY=("\${filtered[@]}")
276
301
  fi
302
+ if [[ -n "$insertion_prefix" ]]; then
303
+ local __prefix_index
304
+ for __prefix_index in "\${!COMPREPLY[@]}"; do
305
+ COMPREPLY[$__prefix_index]="$insertion_prefix\${COMPREPLY[$__prefix_index]}"
306
+ done
307
+ fi
308
+ if [[ "$__prefix_aware" == "1" ]]; then
309
+ COMPREPLY=("\${__saved_reply[@]}" "\${COMPREPLY[@]}")
310
+ fi
311
+ current="$__saved_current"
277
312
  else
278
313
  # Regular literal completion
279
314
  COMPREPLY+=("$line")
@@ -293,7 +328,7 @@ complete -F _${programName} -- ${programName}
293
328
  const extensions = encodeExtensions(suggestion.extensions);
294
329
  const hidden = suggestion.includeHidden ? "1" : "0";
295
330
  const pattern = encodePattern(suggestion.pattern ?? "");
296
- yield `__FILE__:${suggestion.type}:${extensions}:${pattern}:${hidden}`;
331
+ yield `__FILE__:${suggestion.type}:${extensions}:${pattern}:${hidden}${encodeFilePrefix(suggestion)}`;
297
332
  }
298
333
  i++;
299
334
  }
@@ -343,9 +378,20 @@ function _${programName.replace(/[^a-zA-Z0-9]/g, "_")} () {
343
378
 
344
379
  if [[ "\$value" == __FILE__:* ]]; then
345
380
  # Parse file completion directive: __FILE__:type:extensions:pattern:hidden
346
- local type extensions pattern hidden
347
- IFS=':' read -r _ type extensions pattern hidden <<< "\$value"
348
- pattern="\${pattern//%3A/:}"; pattern="\${pattern//%25/%}"
381
+ local type extensions pattern hidden insertion_prefix
382
+ IFS=':' read -r _ type extensions pattern hidden insertion_prefix <<< "\$value"
383
+ local __decoded_pattern="\$pattern"
384
+ __decoded_pattern="\${__decoded_pattern//\\%3A/:}"; __decoded_pattern="\${__decoded_pattern//\\%25/%}"
385
+ insertion_prefix="\${insertion_prefix//\\%3A/:}"; insertion_prefix="\${insertion_prefix//\\%25/%}"
386
+ local __saved_prefix="\$PREFIX" __saved_iprefix="\$IPREFIX"
387
+ if [[ -n "\$insertion_prefix" && "\$__decoded_pattern" == "\$insertion_prefix"* && "\$PREFIX" == "\$insertion_prefix"* ]]; then
388
+ pattern="\${__decoded_pattern#"\$insertion_prefix"}"
389
+ PREFIX="\${PREFIX#"\$insertion_prefix"}"
390
+ IPREFIX="\$IPREFIX\$insertion_prefix"
391
+ else
392
+ # Preserve legacy decoding for directives without active metadata.
393
+ pattern="\${pattern//%3A/:}"; pattern="\${pattern//%25/%}"
394
+ fi
349
395
  has_file_completion=1
350
396
 
351
397
  # Native completion helpers expect normal zsh glob grouping even if
@@ -364,7 +410,6 @@ function _${programName.replace(/[^a-zA-Z0-9]/g, "_")} () {
364
410
  # _directories enumerate the pattern's directory instead of the
365
411
  # current word. If the user has already typed beyond the pattern,
366
412
  # keep PREFIX unchanged for incremental narrowing.
367
- local __saved_prefix="\$PREFIX"
368
413
  if [[ -n "\$pattern" ]]; then
369
414
  # Normalize leading ./ so that ./src/ma matches pattern src/
370
415
  local __norm_prefix="\${PREFIX#./}"
@@ -421,6 +466,7 @@ function _${programName.replace(/[^a-zA-Z0-9]/g, "_")} () {
421
466
 
422
467
  # Restore PREFIX and glob_dots to their previous state
423
468
  PREFIX="\$__saved_prefix"
469
+ IPREFIX="\$__saved_iprefix"
424
470
  if [[ "\$__was_sh_glob" == "1" ]]; then setopt sh_glob; else unsetopt sh_glob; fi
425
471
  if [[ "\$__was_glob_dots" == "1" ]]; then setopt glob_dots; else unsetopt glob_dots; fi
426
472
  else
@@ -463,7 +509,7 @@ fi
463
509
  const hidden = suggestion.includeHidden ? "1" : "0";
464
510
  const description = suggestion.description == null ? "" : sanitizeForTransport(formatMessage(suggestion.description, { colors: false }));
465
511
  const pattern = encodePattern(suggestion.pattern ?? "");
466
- yield `__FILE__:${suggestion.type}:${extensions}:${pattern}:${hidden}\0${description}\0`;
512
+ yield `__FILE__:${suggestion.type}:${extensions}:${pattern}:${hidden}${encodeFilePrefix(suggestion)}\0${description}\0`;
467
513
  }
468
514
  }
469
515
  };
@@ -501,6 +547,21 @@ ${escapedArgs ? ` set -l output (${programName} ${escapedArgs} $prev $current
501
547
  set -l extensions $parts[3]
502
548
  set -l pattern (string replace -a '%25' '%' -- (string replace -a '%3A' ':' -- $parts[4]))
503
549
  set -l hidden $parts[5]
550
+ set -l insertion_prefix ""
551
+ if test (count $parts) -ge 6
552
+ set insertion_prefix (string replace -a '%25' '%' -- (string replace -a '%3A' ':' -- $parts[6]))
553
+ end
554
+ set -l saved_current "$current"
555
+ set -l prefix_length (string length -- "$insertion_prefix")
556
+ if test $prefix_length -gt 0
557
+ and test (string sub -l $prefix_length -- "$pattern") = "$insertion_prefix"
558
+ and test (string sub -l $prefix_length -- "$current") = "$insertion_prefix"
559
+ set -l start (math $prefix_length + 1)
560
+ set pattern (string sub -s $start -- "$pattern")
561
+ set current (string sub -s $start -- "$current")
562
+ else
563
+ set insertion_prefix ""
564
+ end
504
565
 
505
566
  # When a pattern is specified, use it as the glob base instead
506
567
  # of the current word. If the user has already typed beyond the
@@ -722,8 +783,9 @@ ${escapedArgs ? ` set -l output (${programName} ${escapedArgs} $prev $current
722
783
 
723
784
  # Output file completions
724
785
  for item in $items
725
- echo $item
786
+ echo "$insertion_prefix$item"
726
787
  end
788
+ set current "$saved_current"
727
789
  else
728
790
  # Regular literal completion - split by tab
729
791
  set -l parts (string split \\t -- $line)
@@ -753,7 +815,7 @@ complete -c ${programName} -f -a '(${functionName})'
753
815
  const hidden = suggestion.includeHidden ? "1" : "0";
754
816
  const description = suggestion.description == null ? "" : sanitizeForTransport(formatMessage(suggestion.description, { colors: false }));
755
817
  const pattern = encodePattern(suggestion.pattern ?? "");
756
- yield `__FILE__:${suggestion.type}:${extensions}:${pattern}:${hidden}\t${description}`;
818
+ yield `__FILE__:${suggestion.type}:${extensions}:${pattern}:${hidden}${encodeFilePrefix(suggestion)}\t${description}`;
757
819
  }
758
820
  i++;
759
821
  }
@@ -866,6 +928,10 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
866
928
  let pattern = ($parts | get 3 | str replace -a '%3A' ':' | str replace -a '%25' '%')
867
929
  let hidden = ($parts | get 4) == '1'
868
930
 
931
+ let insertion_prefix = if ($parts | length) > 5 {
932
+ $parts | get 5 | str replace -a '%3A' ':' | str replace -a '%25' '%'
933
+ } else { "" }
934
+
869
935
  # Extract prefix from the last argument if it exists
870
936
  let prefix = if ($final_args | length) > 0 {
871
937
  $final_args | last
@@ -873,6 +939,11 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
873
939
  ""
874
940
  }
875
941
 
942
+ let insertion_prefix = if ($insertion_prefix | is-not-empty) and ($pattern | str starts-with $insertion_prefix) and ($prefix | str starts-with $insertion_prefix) { $insertion_prefix } else { "" }
943
+ let prefix_length = ($insertion_prefix | str length)
944
+ let pattern = if $prefix_length > 0 { $pattern | str substring $prefix_length.. } else { $pattern }
945
+ let prefix = if $prefix_length > 0 { $prefix | str substring $prefix_length.. } else { $prefix }
946
+
876
947
  # When a pattern is specified, use it as the glob base instead of
877
948
  # the user-typed prefix. If the user has already typed beyond the
878
949
  # pattern (e.g., pattern="src/" and prefix="src/ma"), keep the
@@ -1018,7 +1089,7 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
1018
1089
  } else {
1019
1090
  $dir_prefix + ($item.name | path basename)
1020
1091
  }
1021
- { value: $name }
1092
+ { value: ($insertion_prefix + $name) }
1022
1093
  }
1023
1094
  } else {
1024
1095
  # Regular literal completion - split by tab
@@ -1075,7 +1146,7 @@ ${functionName}-external
1075
1146
  const hidden = suggestion.includeHidden ? "1" : "0";
1076
1147
  const description = suggestion.description == null ? "" : sanitizeForTransport(formatMessage(suggestion.description, { colors: false }));
1077
1148
  const pattern = encodePattern(suggestion.pattern ?? "");
1078
- yield `__FILE__:${suggestion.type}:${extensions}:${pattern}:${hidden}\t${description}`;
1149
+ yield `__FILE__:${suggestion.type}:${extensions}:${pattern}:${hidden}${encodeFilePrefix(suggestion)}\t${description}`;
1079
1150
  }
1080
1151
  i++;
1081
1152
  }
@@ -1142,11 +1213,17 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
1142
1213
  if (\$line -match '^__FILE__:') {
1143
1214
  # Parse file completion directive: __FILE__:type:extensions:pattern:hidden
1144
1215
  \$directive = (\$line -split "\`t")[0]
1145
- \$parts = \$directive -split ':', 5
1216
+ \$parts = \$directive -split ':', 6
1146
1217
  \$type = \$parts[1]
1147
1218
  \$extensions = \$parts[2]
1148
1219
  \$pattern = \$parts[3] -replace '%3A', ':' -replace '%25', '%'
1149
1220
  \$hidden = \$parts[4] -eq '1'
1221
+ \$insertionPrefix = if (\$parts.Length -gt 5) { \$parts[5] -replace '%3A', ':' -replace '%25', '%' } else { '' }
1222
+ \$matchingWord = \$wordToComplete
1223
+ if (\$insertionPrefix -and \$pattern.StartsWith(\$insertionPrefix, [System.StringComparison]::Ordinal) -and \$matchingWord.StartsWith(\$insertionPrefix, [System.StringComparison]::Ordinal)) {
1224
+ \$pattern = \$pattern.Substring(\$insertionPrefix.Length)
1225
+ \$matchingWord = \$matchingWord.Substring(\$insertionPrefix.Length)
1226
+ } else { \$insertionPrefix = '' }
1150
1227
 
1151
1228
  # When a pattern is specified, use it as the file matching
1152
1229
  # prefix instead of the current word. If the user has
@@ -1155,7 +1232,7 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
1155
1232
  # comparing so that Windows backslashes match forward slashes
1156
1233
  # in the transported pattern.
1157
1234
  \$normalizedPattern = if (\$pattern) { \$pattern.Replace('\\', '/') -replace '^\\./','' } else { '' }
1158
- \$normalizedWord = if (\$wordToComplete) { \$wordToComplete.Replace('\\', '/') -replace '^\\./','' } else { '' }
1235
+ \$normalizedWord = if (\$matchingWord) { \$matchingWord.Replace('\\', '/') -replace '^\\./','' } else { '' }
1159
1236
  \$comparison = if (\$IsWindows) { [System.StringComparison]::OrdinalIgnoreCase } else { [System.StringComparison]::Ordinal }
1160
1237
  # For wildcard patterns, compare only the directory prefix
1161
1238
  \$comparePattern = \$normalizedPattern
@@ -1167,12 +1244,12 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
1167
1244
  \$comparePattern = ''
1168
1245
  }
1169
1246
  }
1170
- \$prefix = if ((\$comparePattern -or \$normalizedWord) -and \$normalizedWord -and \$normalizedWord.StartsWith(\$comparePattern, \$comparison) -and \$normalizedWord.Length -ge \$comparePattern.Length -and \$wordToComplete -ne \$pattern) {
1171
- \$wordToComplete
1247
+ \$prefix = if ((\$comparePattern -or \$normalizedWord) -and \$normalizedWord -and \$normalizedWord.StartsWith(\$comparePattern, \$comparison) -and \$normalizedWord.Length -ge \$comparePattern.Length -and \$matchingWord -ne \$pattern) {
1248
+ \$matchingWord
1172
1249
  } elseif (\$pattern) {
1173
1250
  \$pattern
1174
- } elseif (\$wordToComplete) {
1175
- \$wordToComplete
1251
+ } elseif (\$matchingWord) {
1252
+ \$matchingWord
1176
1253
  } else { '' }
1177
1254
 
1178
1255
  # Use -Force to include hidden files when requested, or when
@@ -1247,7 +1324,7 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
1247
1324
  \$completionText = if (\$_.PSIsContainer) { "\$dirPrefix\$(\$_.Name)/" } else { "\$dirPrefix\$(\$_.Name)" }
1248
1325
  \$itemType = if (\$_.PSIsContainer) { 'Directory' } else { 'File' }
1249
1326
  [System.Management.Automation.CompletionResult]::new(
1250
- \$completionText,
1327
+ (\$insertionPrefix + \$completionText),
1251
1328
  \$completionText,
1252
1329
  'ParameterValue',
1253
1330
  \$itemType
@@ -1287,7 +1364,7 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
1287
1364
  const hidden = suggestion.includeHidden ? "1" : "0";
1288
1365
  const description = suggestion.description == null ? "" : sanitizeForTransport(formatMessage(suggestion.description, { colors: false }));
1289
1366
  const pattern = encodePattern(suggestion.pattern ?? "");
1290
- yield `__FILE__:${suggestion.type}:${extensions}:${pattern}:${hidden}\t[file]\t${description}`;
1367
+ yield `__FILE__:${suggestion.type}:${extensions}:${pattern}:${hidden}${encodeFilePrefix(suggestion)}\t[file]\t${description}`;
1291
1368
  }
1292
1369
  i++;
1293
1370
  }
@@ -637,6 +637,14 @@ type Suggestion = {
637
637
  * The current prefix/pattern for fallback when native completion is unavailable.
638
638
  */
639
639
  readonly pattern?: string;
640
+ /**
641
+ * Literal text at the start of `pattern` that is preserved on insertion
642
+ * but excluded from native filesystem matching, such as `output=`.
643
+ * Shell adapters ignore it if it does not lead both the pattern and
644
+ * the current word. Regenerate installed scripts to use this field.
645
+ * @since 1.1.13
646
+ */
647
+ readonly prefix?: string;
640
648
  /**
641
649
  * The type of file system entries to complete.
642
650
  */
@@ -637,6 +637,14 @@ type Suggestion = {
637
637
  * The current prefix/pattern for fallback when native completion is unavailable.
638
638
  */
639
639
  readonly pattern?: string;
640
+ /**
641
+ * Literal text at the start of `pattern` that is preserved on insertion
642
+ * but excluded from native filesystem matching, such as `output=`.
643
+ * Shell adapters ignore it if it does not lead both the pattern and
644
+ * the current word. Regenerate installed scripts to use this field.
645
+ * @since 1.1.13
646
+ */
647
+ readonly prefix?: string;
640
648
  /**
641
649
  * The type of file system entries to complete.
642
650
  */
@@ -1311,6 +1311,7 @@ function multiple(parser, options = {}) {
1311
1311
  "file",
1312
1312
  suggestion.type,
1313
1313
  suggestion.pattern ?? "",
1314
+ suggestion.prefix ?? "",
1314
1315
  suggestion.includeHidden === true,
1315
1316
  suggestion.extensions == null ? "" : suggestion.extensions.toSorted().join("\0"),
1316
1317
  description
package/dist/modifiers.js CHANGED
@@ -1311,6 +1311,7 @@ function multiple(parser, options = {}) {
1311
1311
  "file",
1312
1312
  suggestion.type,
1313
1313
  suggestion.pattern ?? "",
1314
+ suggestion.prefix ?? "",
1314
1315
  suggestion.includeHidden === true,
1315
1316
  suggestion.extensions == null ? "" : suggestion.extensions.toSorted().join("\0"),
1316
1317
  description
@@ -251,7 +251,7 @@ function* getSuggestionsWithDependency(valueParser, prefix, dependencyRegistry,
251
251
  * @internal
252
252
  */
253
253
  function* suggestOptionSync(optionNames$1, valueParser, hidden, description, context, prefix) {
254
- if (hidden) return;
254
+ if (hidden || context.optionsTerminated) return;
255
255
  const equalsIndex = prefix.indexOf("=");
256
256
  if (equalsIndex >= 0) {
257
257
  const optionPart = prefix.slice(0, equalsIndex);
@@ -336,7 +336,7 @@ async function* getSuggestionsWithDependencyAsync(valueParser, prefix, dependenc
336
336
  * @internal
337
337
  */
338
338
  async function* suggestOptionAsync(optionNames$1, valueParser, hidden, description, context, prefix) {
339
- if (hidden) return;
339
+ if (hidden || context.optionsTerminated) return;
340
340
  const equalsIndex = prefix.indexOf("=");
341
341
  if (equalsIndex >= 0) {
342
342
  const optionPart = prefix.slice(0, equalsIndex);
@@ -915,8 +915,8 @@ function flag(...args) {
915
915
  error: require_message.message`${require_message.optionNames(optionNames$1)}: ${state.error}`
916
916
  };
917
917
  },
918
- suggest(_context, prefix) {
919
- if (require_usage.isSuggestionHidden(options.hidden)) return [];
918
+ suggest(context, prefix) {
919
+ if (require_usage.isSuggestionHidden(options.hidden) || context.optionsTerminated) return [];
920
920
  const suggestions = [];
921
921
  if (prefix.startsWith("--") || prefix.startsWith("-") || prefix.startsWith("/") || prefix.startsWith("+")) {
922
922
  for (const optionName$1 of optionNames$1) if (optionName$1.startsWith(prefix)) {
@@ -251,7 +251,7 @@ function* getSuggestionsWithDependency(valueParser, prefix, dependencyRegistry,
251
251
  * @internal
252
252
  */
253
253
  function* suggestOptionSync(optionNames$1, valueParser, hidden, description, context, prefix) {
254
- if (hidden) return;
254
+ if (hidden || context.optionsTerminated) return;
255
255
  const equalsIndex = prefix.indexOf("=");
256
256
  if (equalsIndex >= 0) {
257
257
  const optionPart = prefix.slice(0, equalsIndex);
@@ -336,7 +336,7 @@ async function* getSuggestionsWithDependencyAsync(valueParser, prefix, dependenc
336
336
  * @internal
337
337
  */
338
338
  async function* suggestOptionAsync(optionNames$1, valueParser, hidden, description, context, prefix) {
339
- if (hidden) return;
339
+ if (hidden || context.optionsTerminated) return;
340
340
  const equalsIndex = prefix.indexOf("=");
341
341
  if (equalsIndex >= 0) {
342
342
  const optionPart = prefix.slice(0, equalsIndex);
@@ -915,8 +915,8 @@ function flag(...args) {
915
915
  error: message`${optionNames(optionNames$1)}: ${state.error}`
916
916
  };
917
917
  },
918
- suggest(_context, prefix) {
919
- if (isSuggestionHidden(options.hidden)) return [];
918
+ suggest(context, prefix) {
919
+ if (isSuggestionHidden(options.hidden) || context.optionsTerminated) return [];
920
920
  const suggestions = [];
921
921
  if (prefix.startsWith("--") || prefix.startsWith("-") || prefix.startsWith("/") || prefix.startsWith("+")) {
922
922
  for (const optionName$1 of optionNames$1) if (optionName$1.startsWith(prefix)) {
@@ -257,22 +257,27 @@ function createErrorWithSuggestions(baseError, invalidInput, usage, type = "both
257
257
  *
258
258
  * For literal suggestions, the text itself is used as the key.
259
259
  * For file suggestions, a composite key is created from the type,
260
- * extensions, and pattern.
260
+ * extensions, pattern, and insertion prefix.
261
261
  *
262
262
  * @param suggestion The suggestion to create a key for
263
263
  * @returns A string key that uniquely identifies this suggestion
264
264
  * @internal
265
265
  */
266
266
  function getSuggestionKey(suggestion) {
267
- if (suggestion.kind === "literal") return suggestion.text;
268
- return `__FILE__:${suggestion.type}:${suggestion.extensions?.toSorted().join(",") ?? ""}:${suggestion.pattern ?? ""}`;
267
+ if (suggestion.kind === "literal") return `literal:${suggestion.text}`;
268
+ return `file:${JSON.stringify([
269
+ suggestion.type,
270
+ suggestion.extensions?.toSorted().join(",") ?? "",
271
+ suggestion.pattern ?? "",
272
+ suggestion.prefix ?? ""
273
+ ])}`;
269
274
  }
270
275
  /**
271
276
  * Removes duplicate suggestions from an array while preserving order.
272
277
  *
273
278
  * Suggestions are considered duplicates if they have the same key:
274
279
  * - Literal suggestions: same text
275
- * - File suggestions: same type, extensions, and pattern
280
+ * - File suggestions: same type, extensions, pattern, and insertion prefix
276
281
  *
277
282
  * The first occurrence of each unique suggestion is kept. For file
278
283
  * suggestions, `includeHidden` is merged across duplicates: if any
@@ -159,7 +159,7 @@ declare function createErrorWithSuggestions(baseError: Message, invalidInput: st
159
159
  *
160
160
  * Suggestions are considered duplicates if they have the same key:
161
161
  * - Literal suggestions: same text
162
- * - File suggestions: same type, extensions, and pattern
162
+ * - File suggestions: same type, extensions, pattern, and insertion prefix
163
163
  *
164
164
  * The first occurrence of each unique suggestion is kept. For file
165
165
  * suggestions, `includeHidden` is merged across duplicates: if any
@@ -159,7 +159,7 @@ declare function createErrorWithSuggestions(baseError: Message, invalidInput: st
159
159
  *
160
160
  * Suggestions are considered duplicates if they have the same key:
161
161
  * - Literal suggestions: same text
162
- * - File suggestions: same type, extensions, and pattern
162
+ * - File suggestions: same type, extensions, pattern, and insertion prefix
163
163
  *
164
164
  * The first occurrence of each unique suggestion is kept. For file
165
165
  * suggestions, `includeHidden` is merged across duplicates: if any
@@ -257,22 +257,27 @@ function createErrorWithSuggestions(baseError, invalidInput, usage, type = "both
257
257
  *
258
258
  * For literal suggestions, the text itself is used as the key.
259
259
  * For file suggestions, a composite key is created from the type,
260
- * extensions, and pattern.
260
+ * extensions, pattern, and insertion prefix.
261
261
  *
262
262
  * @param suggestion The suggestion to create a key for
263
263
  * @returns A string key that uniquely identifies this suggestion
264
264
  * @internal
265
265
  */
266
266
  function getSuggestionKey(suggestion) {
267
- if (suggestion.kind === "literal") return suggestion.text;
268
- return `__FILE__:${suggestion.type}:${suggestion.extensions?.toSorted().join(",") ?? ""}:${suggestion.pattern ?? ""}`;
267
+ if (suggestion.kind === "literal") return `literal:${suggestion.text}`;
268
+ return `file:${JSON.stringify([
269
+ suggestion.type,
270
+ suggestion.extensions?.toSorted().join(",") ?? "",
271
+ suggestion.pattern ?? "",
272
+ suggestion.prefix ?? ""
273
+ ])}`;
269
274
  }
270
275
  /**
271
276
  * Removes duplicate suggestions from an array while preserving order.
272
277
  *
273
278
  * Suggestions are considered duplicates if they have the same key:
274
279
  * - Literal suggestions: same text
275
- * - File suggestions: same type, extensions, and pattern
280
+ * - File suggestions: same type, extensions, pattern, and insertion prefix
276
281
  *
277
282
  * The first occurrence of each unique suggestion is kept. For file
278
283
  * suggestions, `includeHidden` is merged across duplicates: if any
@@ -602,7 +602,8 @@ function prefixKeyValueSuggestion(suggestion, prefix, suffix = "") {
602
602
  const text$1 = `${prefix}${textValue}${suffix}`;
603
603
  if (suggestion.kind === "file" && suffix === "") return {
604
604
  ...suggestion,
605
- pattern: text$1
605
+ pattern: text$1,
606
+ prefix: `${prefix}${suggestion.prefix ?? ""}`
606
607
  };
607
608
  return suggestion.description == null ? {
608
609
  kind: "literal",
@@ -602,7 +602,8 @@ function prefixKeyValueSuggestion(suggestion, prefix, suffix = "") {
602
602
  const text$1 = `${prefix}${textValue}${suffix}`;
603
603
  if (suggestion.kind === "file" && suffix === "") return {
604
604
  ...suggestion,
605
- pattern: text$1
605
+ pattern: text$1,
606
+ prefix: `${prefix}${suggestion.prefix ?? ""}`
606
607
  };
607
608
  return suggestion.description == null ? {
608
609
  kind: "literal",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optique/core",
3
- "version": "1.1.11",
3
+ "version": "1.1.13-dev.2823",
4
4
  "description": "Type-safe combinatorial command-line interface parser",
5
5
  "keywords": [
6
6
  "CLI",
@@ -196,7 +196,7 @@
196
196
  },
197
197
  "sideEffects": false,
198
198
  "devDependencies": {
199
- "@optique/env": "1.1.11",
199
+ "@optique/env": "1.1.13-dev.2823+d4c34d2f",
200
200
  "@types/node": "^24.0.0",
201
201
  "fast-check": "^4.7.0",
202
202
  "tsdown": "^0.13.0",