@optique/core 1.0.15-dev.2809 → 1.0.15-dev.2813

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.
@@ -1,3 +1,4 @@
1
+ const require_joined_file_suggestion = require('./internal/joined-file-suggestion.cjs');
1
2
  const require_message = require('./message.cjs');
2
3
 
3
4
  //#region src/completion.ts
@@ -40,6 +41,11 @@ function validateProgramName(programName) {
40
41
  function encodePattern(pattern) {
41
42
  return pattern.replace(/%/g, "%25").replace(/:/g, "%3A");
42
43
  }
44
+ /** Escapes literal records that could otherwise be read as Bash directives. */
45
+ function encodeBashLiteral(value) {
46
+ const text = sanitizeForTransport(value);
47
+ return /^(?:__FILE__|__JOINED_FILE__|__LITERAL__):/.test(text) ? `__LITERAL__:${text}` : text;
48
+ }
43
49
  function encodeExtensions(extensions) {
44
50
  return extensions?.map((ext) => ext.replace(/^\./, "")).join(",") ?? "";
45
51
  }
@@ -106,8 +112,227 @@ function _${programName} () {
106
112
  local __tilde_pattern='^(~[a-zA-Z0-9_.+-]*)(/.*)$'
107
113
  local __tilde_glob_pattern='^(~[a-zA-Z0-9_.+-]*)(/.*)?$'
108
114
  local prev=("\${COMP_WORDS[@]:1:COMP_CWORD-1}")
109
- while IFS= read -r line; do
110
- if [[ "$line" == __FILE__:* ]]; then
115
+ # Bash splits unquoted '=' at its default word breaks. Reassemble only
116
+ # adjacent fragments, including further word breaks inside a joined value,
117
+ # leaving whitespace-separated arguments intact.
118
+ local __plain_current="$current" __remove="" __word __joined=0
119
+ local __raw_joined_current="$__raw_current"
120
+ local -a __raw_offsets=() __raw_quotes=()
121
+ if [[ \${COMP_LINE+x} ]]; then
122
+ local -a __words=()
123
+ local __rest="$COMP_LINE" __previous="" __space __valid=1 __i __j=0
124
+ local __shell_quote="" __shell_escape=0 __boundary=0 __previous_boundary=0 __scan __symbol __shell_dollar=0
125
+ # Reconstruct the prefix at the cursor, leaving any suffix to Readline.
126
+ if [[ \${COMP_POINT+x} ]]; then
127
+ __rest="\${COMP_LINE:0:COMP_POINT}"
128
+ fi
129
+ for ((__i=0; __i<=COMP_CWORD; __i++)); do
130
+ __word="\${COMP_WORDS[__i]}"
131
+ __space=0
132
+ while [[ "$__rest" == [[:space:]]* ]]; do
133
+ __space=1
134
+ __rest="\${__rest:1}"
135
+ done
136
+ if (( __i == COMP_CWORD && \${#__rest} < \${#__word} )); then
137
+ __word="\${__word:0:\${#__rest}}"
138
+ fi
139
+ if [[ "\${__rest:0:\${#__word}}" != "$__word" ]]; then
140
+ __valid=0
141
+ break
142
+ fi
143
+ # Quoted or escaped operators belong to words; bare operators separate
144
+ # shell syntax even when no whitespace surrounds a redirection.
145
+ __boundary=0
146
+ for ((__scan=0; __scan<\${#__word}; __scan++)); do
147
+ __symbol="\${__word:__scan:1}"
148
+ if (( __shell_escape )); then
149
+ __shell_escape=0
150
+ elif [[ "$__shell_quote" == ansi ]]; then
151
+ if [[ "$__symbol" == '\\' ]]; then __shell_escape=1
152
+ elif [[ "$__symbol" == "'" ]]; then __shell_quote=""; fi
153
+ elif [[ "$__shell_quote" == "'" ]]; then
154
+ [[ "$__symbol" == "'" ]] && __shell_quote=""
155
+ elif [[ "$__symbol" == '\\' ]]; then
156
+ __shell_escape=1
157
+ elif [[ -n "$__shell_quote" ]]; then
158
+ [[ "$__symbol" == "$__shell_quote" ]] && __shell_quote=""
159
+ else
160
+ case "$__symbol" in
161
+ "'")
162
+ if (( __shell_dollar )); then __shell_quote=ansi
163
+ else __shell_quote="'"; fi ;;
164
+ '"') __shell_quote='"' ;;
165
+ '$') __shell_dollar=1 ;;
166
+ '>'|'<'|'|'|'&'|';'|'('|')') __boundary=1 ;;
167
+ esac
168
+ fi
169
+ [[ "$__symbol" == '$' && -z "$__shell_quote" ]] || __shell_dollar=0
170
+ done
171
+ if (( __i > 1 && !__space && !__boundary && !__previous_boundary )) &&
172
+ { [[ -n "$__word" && -z "\${__word//=/}" ]] ||
173
+ [[ -n "$__previous" && -z "\${__previous//=/}" ]] ||
174
+ [[ "\${__words[__j-1]}" == [-/+]* ]] ||
175
+ [[ "$__word" == =* && "\${__words[__j-1]}" == [-/+]* ]]; }; then
176
+ __words[__j-1]+="$__word"
177
+ else
178
+ __words[__j]="$__word"
179
+ ((++__j))
180
+ fi
181
+ __rest="\${__rest:\${#__word}}"
182
+ __previous="$__word"
183
+ __previous_boundary=$__boundary
184
+ done
185
+ if (( __valid )); then
186
+ current="\${__words[__j-1]}"
187
+ __raw_joined_current="$current"
188
+ prev=("\${__words[@]:1:__j-2}")
189
+ # Bash passes the fragment Readline will replace as the second argument,
190
+ # which is empty when the cursor follows a word-break character.
191
+ if [[ \${2+x} ]]; then
192
+ __remove="\${current:0:\${#current}-\${#2}}"
193
+ elif [[ -n "$__raw_current" && -z "\${__raw_current//=/}" ]]; then
194
+ __remove="$current"
195
+ else
196
+ __remove="\${current:0:\${#current}-\${#__raw_current}}"
197
+ fi
198
+ # Decode joined shell words without evaluating substitutions. Keep quote
199
+ # state across the reassembled fragments and decode the preserved prefix
200
+ # separately so trimming uses the same representation as suggestions.
201
+ __words[__j]="$__remove"
202
+ local __decoded __decoded_option=0 __before_quote __join_quote __char __next __k __ansi __part __digits __code __join_quote_offset
203
+ for ((__i=1; __i<=__j; __i++)); do
204
+ __word="\${__words[__i]}"
205
+ __decoded=""; __join_quote=""; __join_quote_offset=0
206
+ for ((__k=0; __k<\${#__word}; __k++)); do
207
+ __char="\${__word:__k:1}"
208
+ __before_quote="$__join_quote"
209
+ if [[ -z "$__join_quote" ]]; then
210
+ case "$__char" in
211
+ '\\') ((++__k)); __decoded+="\${__word:__k:1}" ;;
212
+ '$')
213
+ if [[ "\${__word:__k+1:1}" == "'" ]]; then
214
+ __join_quote=ansi; __join_quote_offset=\${#__decoded}; __ansi=""; ((++__k))
215
+ elif [[ "\${__word:__k+1:1}" == '"' ]]; then
216
+ __join_quote='"'; __join_quote_offset=\${#__decoded}; ((++__k))
217
+ else __decoded+="$__char"; fi ;;
218
+ "'"|'"') __join_quote="$__char"; __join_quote_offset=\${#__decoded} ;;
219
+ *) __decoded+="$__char" ;;
220
+ esac
221
+ elif [[ "$__join_quote" == ansi ]]; then
222
+ if [[ "$__char" == "'" ]]; then
223
+ printf -v __part '%b' "$__ansi"
224
+ __decoded+="$__part"; __join_quote=""
225
+ elif [[ "$__char" == '\\' ]]; then
226
+ ((++__k)); __next="\${__word:__k:1}"
227
+ case "$__next" in
228
+ [0-7])
229
+ __digits="$__next"
230
+ while (( \${#__digits} < 3 )) && [[ "\${__word:__k+1:1}" == [0-7] ]]; do
231
+ ((++__k)); __digits+="\${__word:__k:1}"
232
+ done
233
+ __ansi+='\\0'"$__digits" ;;
234
+ c)
235
+ __next="\${__word:__k+1:1}"
236
+ if [[ -n "$__next" && "$__next" != "'" ]]; then
237
+ ((++__k)); printf -v __code '%d' "'$__next"
238
+ if [[ "$__next" == '?' ]]; then __code=127
239
+ else __code=$((__code & 31)); fi
240
+ printf -v __part '\\\\%03o' "$__code"
241
+ __ansi+="$__part"
242
+ else __ansi+='\\\\c'; fi ;;
243
+ "'"|'"'|'?') __ansi+="$__next" ;;
244
+ *) __ansi+='\\'"$__next" ;;
245
+ esac
246
+ else __ansi+="$__char"; fi
247
+ elif [[ "$__join_quote" == "'" ]]; then
248
+ if [[ "$__char" == "'" ]]; then __join_quote=""
249
+ else __decoded+="$__char"; fi
250
+ else
251
+ case "$__char" in
252
+ '"') __join_quote="" ;;
253
+ '\\')
254
+ __next="\${__word:__k+1:1}"
255
+ case "$__next" in
256
+ '$'|'\`'|'"'|'\\') ((++__k)); __decoded+="$__next" ;;
257
+ *) __decoded+="$__char" ;;
258
+ esac ;;
259
+ *) __decoded+="$__char" ;;
260
+ esac
261
+ fi
262
+ if (( __i == __j - 1 )) &&
263
+ { [[ ! \${__raw_offsets[\${#__decoded}]+x} ]] ||
264
+ [[ -n "$__before_quote" && -z "$__join_quote" ]]; }; then
265
+ __raw_offsets[\${#__decoded}]=$((__k + 1))
266
+ __raw_quotes[\${#__decoded}]="$__join_quote"
267
+ fi
268
+ done
269
+ if [[ "$__join_quote" == ansi ]]; then
270
+ printf -v __part '%b' "$__ansi"
271
+ __decoded+="$__part"
272
+ fi
273
+ # Shell syntax may precede the option prefix. Decide from decoded text,
274
+ # retaining the existing native-file handling for non-option words.
275
+ if [[ "$__decoded" == [-/+]* ]] || (( __i == __j && __decoded_option )); then
276
+ if (( __i == __j - 1 )); then
277
+ __decoded_option=1
278
+ __quote="$__join_quote"
279
+ [[ "$__quote" == ansi ]] && __quote="'"
280
+ __quote_offset=$__join_quote_offset
281
+ fi
282
+ __words[__i]="$__decoded"
283
+ fi
284
+ done
285
+ if [[ "\${__words[__j-1]}" == -*=* || "\${__words[__j-1]}" == /*:* || "\${__words[__j-1]}" != "$__raw_current" ]]; then
286
+ current="\${__words[__j-1]}"
287
+ __joined=1
288
+ else
289
+ current="$__plain_current"
290
+ __words[__j]=""
291
+ fi
292
+ prev=("\${__words[@]:1:__j-2}")
293
+ __remove="\${__words[__j]}"
294
+ fi
295
+ fi
296
+
297
+ # Joined file directives retain the exact option prefix and metadata chosen
298
+ # by the parser; do not infer option boundaries through speculative retries.
299
+ local __protocol_current="$__raw_current"
300
+ (( __joined )) && __protocol_current="$current"
301
+ local -a __lines=() __replies=()
302
+ local line __reply_prefix="" __file_current="$current" __literal=0
303
+ while IFS= read -r line || [[ -n "$line" ]]; do
304
+ [[ -n "$line" ]] && __lines+=("$line")
305
+ done < <(${programName} ${escapedArgs} "\${prev[@]}" "$__protocol_current" 2>/dev/null)
306
+ for line in "\${__lines[@]}"; do
307
+ COMPREPLY=()
308
+ __reply_prefix=""
309
+ __file_current="$current"
310
+ __literal=0
311
+ if [[ "$line" == __LITERAL__:* ]]; then
312
+ line="\${line#__LITERAL__:}"
313
+ __literal=1
314
+ elif [[ "$line" == __JOINED_FILE__:* ]]; then
315
+ line="\${line#__JOINED_FILE__:}"
316
+ __reply_prefix="\${line%%:*}"
317
+ __reply_prefix="\${__reply_prefix//%3A/:}"
318
+ __reply_prefix="\${__reply_prefix//%25/%}"
319
+ line="__FILE__:\${line#*:}"
320
+ __file_current="\${current:\${#__reply_prefix}}"
321
+ fi
322
+ local __raw_file_current="$__raw_current" __file_quote_offset=$__quote_offset __file_tilde_quoted=0
323
+ if [[ -n "$__reply_prefix" ]]; then
324
+ local __raw_offset="\${__raw_offsets[\${#__reply_prefix}]:-\${#__reply_prefix}}"
325
+ __raw_file_current="\${__raw_joined_current:__raw_offset}"
326
+ # Quoting may start before the option name and remain active at the
327
+ # delimiter. Keep that context when deciding whether tilde is literal.
328
+ if [[ -n "\${__raw_quotes[\${#__reply_prefix}]-}" ]]; then
329
+ __file_tilde_quoted=1
330
+ fi
331
+ if (( __file_quote_offset >= \${#__reply_prefix} )); then
332
+ __file_quote_offset=$((__file_quote_offset - \${#__reply_prefix}))
333
+ fi
334
+ fi
335
+ if [[ $__literal == 0 && "$line" == __FILE__:* ]]; then
111
336
  # Parse file completion directive: __FILE__:type:extensions:pattern:hidden
112
337
  IFS=':' read -r _ type extensions pattern hidden <<< "$line"
113
338
  extensions="\${extensions//,\\./,}"; extensions="\${extensions#.}"
@@ -116,7 +341,8 @@ function _${programName} () {
116
341
 
117
342
  # path() echoes the raw shell word as its pattern. Once dequoted,
118
343
  # use the literal current-word prefix instead of the escaped pattern.
119
- if [[ "$pattern" == "$__raw_current" && "$current" != "$__raw_current" ]]; then
344
+ if [[ ( "$pattern" == "$__raw_file_current" && "$__file_current" != "$__raw_file_current" ) ||
345
+ ( -n "$__reply_prefix" && "$pattern" == "$__file_current" ) ]]; then
120
346
  pattern=""
121
347
  fi
122
348
 
@@ -134,12 +360,12 @@ function _${programName} () {
134
360
  unset GLOBIGNORE
135
361
 
136
362
  # Expand tilde prefix for file globbing
137
- local __glob_current="$current" __tilde_prefix="" __tilde_expanded=""
138
- if [[ "$__raw_current" == "~"* && "$current" =~ $__tilde_pattern ]]; then
363
+ local __glob_current="$__file_current" __tilde_prefix="" __tilde_expanded=""
364
+ if [[ $__file_tilde_quoted == 0 && "$__raw_file_current" == "~"* && "$__file_current" =~ $__tilde_pattern ]]; then
139
365
  __tilde_prefix="\${BASH_REMATCH[1]}"
140
366
  eval "__tilde_expanded=\$__tilde_prefix" 2>/dev/null || true
141
367
  if [[ -n "$__tilde_expanded" && "$__tilde_expanded" != "$__tilde_prefix" ]]; then
142
- __glob_current="\${__tilde_expanded}\${current#\$__tilde_prefix}"
368
+ __glob_current="\${__tilde_expanded}\${__file_current#\$__tilde_prefix}"
143
369
  else
144
370
  __tilde_prefix=""
145
371
  fi
@@ -153,7 +379,7 @@ function _${programName} () {
153
379
  local __from_pattern=0
154
380
  if [[ -n "$pattern" ]]; then
155
381
  # Normalize leading ./ so that ./src/ma matches pattern src/
156
- local __norm_current="\${current#./}"
382
+ local __norm_current="\${__file_current#./}"
157
383
  local __norm_pattern="\${pattern#./}"
158
384
  # For wildcard patterns, compare only the directory prefix before
159
385
  # any wildcards so that typing src/ma narrows src/*.ts correctly
@@ -162,7 +388,7 @@ function _${programName} () {
162
388
  __compare_pattern="\${__compare_pattern%%[\*\?]*}"
163
389
  [[ "$__compare_pattern" == */* ]] && __compare_pattern="\${__compare_pattern%/*}/" || __compare_pattern=""
164
390
  fi
165
- if [[ ( -n "$__compare_pattern" || -n "$__norm_current" ) && \${#__norm_current} -ge \${#__compare_pattern} && "\${__norm_current:0:\${#__compare_pattern}}" == "$__compare_pattern" && "$current" != "$pattern" ]]; then
391
+ if [[ ( -n "$__compare_pattern" || -n "$__norm_current" ) && \${#__norm_current} -ge \${#__compare_pattern} && "\${__norm_current:0:\${#__compare_pattern}}" == "$__compare_pattern" && "$__file_current" != "$pattern" ]]; then
166
392
  # User has typed beyond or an equivalent form of the pattern
167
393
  true
168
394
  else
@@ -296,7 +522,7 @@ function _${programName} () {
296
522
 
297
523
  # Restore tilde prefix in completion results
298
524
  if [[ -n "$__tilde_prefix" &&
299
- ( -z "$__quote" || $__quote_offset -gt 0 ) ]]; then
525
+ ( -z "$__quote" || $__file_quote_offset -gt 0 ) ]]; then
300
526
  local __i
301
527
  for __i in "\${!COMPREPLY[@]}"; do
302
528
  COMPREPLY[\$__i]="\${COMPREPLY[\$__i]/#\$__tilde_expanded/\$__tilde_prefix}"
@@ -313,7 +539,7 @@ function _${programName} () {
313
539
 
314
540
  # Readline appends directory separators when it receives the full path.
315
541
  # Midword quotes still need ours because replies omit the typed prefix.
316
- if [[ -n "$__quote" && $__quote_offset -eq 0 ]]; then
542
+ if [[ -n "$__quote" && $__file_quote_offset -eq 0 ]]; then
317
543
  local __reply_index
318
544
  for ((__reply_index=__reply_start; __reply_index<\${#COMPREPLY[@]}; __reply_index++)); do
319
545
  COMPREPLY[__reply_index]="\${COMPREPLY[__reply_index]%/}"
@@ -322,8 +548,9 @@ function _${programName} () {
322
548
 
323
549
  # Readline replaces only the text after an open quote. Remove the
324
550
  # already typed prefix so it is not inserted a second time.
325
- if [[ -n "$__quote" && $__quote_offset -gt 0 ]]; then
326
- local __quote_prefix="\${current:0:__quote_offset}"
551
+ # Joined replies are trimmed once against __remove after restoring option=.
552
+ if [[ -n "$__quote" && $__file_quote_offset -gt 0 && -z "$__reply_prefix" ]]; then
553
+ local __quote_prefix="\${__file_current:0:__file_quote_offset}"
327
554
  local __reply_index
328
555
  for ((__reply_index=__reply_start; __reply_index<\${#COMPREPLY[@]}; __reply_index++)); do
329
556
  COMPREPLY[__reply_index]="\${COMPREPLY[__reply_index]#"$__quote_prefix"}"
@@ -332,7 +559,7 @@ function _${programName} () {
332
559
 
333
560
  # Readline expands a leading tilde in a returned filename even when the
334
561
  # user did not type one. Keep such local paths explicitly relative.
335
- if [[ "$__raw_current" != "~"* && -z "$__tilde_prefix" ]]; then
562
+ if [[ ( "$__raw_file_current" != "~"* || $__file_tilde_quoted == 1 ) && -z "$__tilde_prefix" ]]; then
336
563
  local __reply_index
337
564
  for ((__reply_index=__reply_start; __reply_index<\${#COMPREPLY[@]}; __reply_index++)); do
338
565
  if [[ "\${COMPREPLY[__reply_index]}" =~ $__tilde_glob_pattern ]]; then
@@ -369,27 +596,30 @@ function _${programName} () {
369
596
  # Filter out hidden files unless requested
370
597
  if [[ "$hidden" != "1" && "$__inside_hidden_path" == "0" && "$__prefix_targets_hidden" == "0" ]]; then
371
598
  local filtered=()
372
- local __name __filter_index __kept_before=0
373
- __filter_index=0
599
+ local __name
374
600
  for item in "\${COMPREPLY[@]}"; do
375
601
  __name="\${item%/}"; __name="\${__name##*/}"
376
602
  if [[ "$__name" != .* ]]; then
377
603
  filtered+=("$item")
378
- if [[ $__filter_index -lt $__reply_start ]]; then
379
- __kept_before=$((__kept_before + 1))
380
- fi
381
604
  fi
382
- __filter_index=$((__filter_index + 1))
383
605
  done
384
606
  COMPREPLY=("\${filtered[@]}")
385
- __reply_start=$__kept_before
386
607
  fi
387
608
  if [[ \${#COMPREPLY[@]} -gt $__reply_start ]]; then __file_mode=1; fi
388
609
  else
389
610
  # Regular literal completion
390
611
  COMPREPLY+=("$line")
391
612
  fi
392
- done < <(${programName} ${escapedArgs} "\${prev[@]}" "$__raw_current" 2>/dev/null)
613
+ for __word in "\${COMPREPLY[@]}"; do
614
+ __replies+=("$__reply_prefix$__word")
615
+ done
616
+ done
617
+ COMPREPLY=()
618
+ for __word in "\${__replies[@]}"; do
619
+ if [[ "\${__word:0:\${#__remove}}" == "$__remove" ]]; then
620
+ COMPREPLY+=("\${__word:\${#__remove}}")
621
+ fi
622
+ done
393
623
  if [[ $__file_mode == 1 && $(type -t compopt) == builtin ]]; then
394
624
  compopt -o filenames 2>/dev/null || true
395
625
  fi
@@ -407,7 +637,17 @@ fi
407
637
  let i = 0;
408
638
  for (const suggestion of suggestions) {
409
639
  if (i > 0) yield "\n";
410
- if (suggestion.kind === "literal") yield sanitizeForTransport(suggestion.text);
640
+ const joined = require_joined_file_suggestion.getJoinedFileSuggestion(suggestion);
641
+ if (joined !== void 0) {
642
+ if (joined.literal && suggestion.kind === "literal") {
643
+ yield encodeBashLiteral(suggestion.text);
644
+ yield "\n";
645
+ }
646
+ for (const [index, { prefix, file }] of joined.files.entries()) {
647
+ if (index > 0) yield "\n";
648
+ yield `__JOINED_FILE__:${encodePattern(prefix)}:${file.type}:${encodeExtensions(file.extensions)}::${file.includeHidden ? "1" : "0"}`;
649
+ }
650
+ } else if (suggestion.kind === "literal") yield encodeBashLiteral(suggestion.text);
411
651
  else {
412
652
  const extensions = encodeExtensions(suggestion.extensions);
413
653
  const hidden = suggestion.includeHidden ? "1" : "0";
@@ -1279,23 +1519,11 @@ Register-ArgumentCompleter -Native -CommandName ${programName} -ScriptBlock {
1279
1519
  \$arguments = @()
1280
1520
  \$commandElements = \$commandAst.CommandElements
1281
1521
 
1282
- # Determine the range of elements to extract
1283
- # Exclude the last element if it matches wordToComplete (partial input case)
1284
- \$maxIndex = \$commandElements.Count - 1
1285
- if (\$commandElements.Count -gt 1) {
1286
- \$lastElement = \$commandElements[\$commandElements.Count - 1]
1287
- \$lastText = if (\$lastElement -is [System.Management.Automation.Language.StringConstantExpressionAst]) {
1288
- \$lastElement.Value
1289
- } else {
1290
- \$lastElement.Extent.Text
1291
- }
1292
- if (\$lastText -eq \$wordToComplete) {
1293
- \$maxIndex = \$commandElements.Count - 2
1294
- }
1295
- }
1296
-
1297
- for (\$i = 1; \$i -le \$maxIndex; \$i++) {
1522
+ # Keep only complete arguments before the cursor. The current word
1523
+ # (including a token ending exactly at the cursor) is appended below.
1524
+ for (\$i = 1; \$i -lt \$commandElements.Count; \$i++) {
1298
1525
  \$element = \$commandElements[\$i]
1526
+ if (\$element.Extent.EndOffset -ge \$cursorPosition) { break }
1299
1527
 
1300
1528
  if (\$element -is [System.Management.Automation.Language.StringConstantExpressionAst]) {
1301
1529
  \$arguments += \$element.Value