@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.
- package/dist/completion.cjs +266 -38
- package/dist/completion.js +266 -38
- package/dist/internal/joined-file-suggestion.cjs +69 -0
- package/dist/internal/joined-file-suggestion.js +66 -0
- package/dist/modifiers.cjs +13 -8
- package/dist/modifiers.js +13 -8
- package/dist/primitives.cjs +23 -26
- package/dist/primitives.js +23 -26
- package/dist/suggestion.cjs +3 -1
- package/dist/suggestion.js +3 -1
- package/package.json +1 -1
package/dist/completion.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getJoinedFileSuggestion } from "./internal/joined-file-suggestion.js";
|
|
1
2
|
import { formatMessage } from "./message.js";
|
|
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
|
-
|
|
110
|
-
|
|
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" == "$
|
|
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="$
|
|
138
|
-
if [[ "$
|
|
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}\${
|
|
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="\${
|
|
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" && "$
|
|
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" || $
|
|
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" && $
|
|
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
|
-
|
|
326
|
-
|
|
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 [[ "$
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
640
|
+
const joined = 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
|
-
#
|
|
1283
|
-
#
|
|
1284
|
-
\$
|
|
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
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
|
|
2
|
+
//#region src/internal/joined-file-suggestion.ts
|
|
3
|
+
const joinedFiles = /* @__PURE__ */ new WeakMap();
|
|
4
|
+
/**
|
|
5
|
+
* Keeps a joined file fallback's origin available to the Bash encoder.
|
|
6
|
+
* Other shells retain the existing literal representation.
|
|
7
|
+
* @param prefix The option name and delimiter.
|
|
8
|
+
* @param file The original file suggestion.
|
|
9
|
+
* @param value The value prefix being completed.
|
|
10
|
+
* @returns The literal fallback with private file metadata.
|
|
11
|
+
* @internal
|
|
12
|
+
*/
|
|
13
|
+
function joinedFileSuggestion(prefix, file, value) {
|
|
14
|
+
const literal = {
|
|
15
|
+
kind: "literal",
|
|
16
|
+
text: prefix + (file.pattern ?? ""),
|
|
17
|
+
description: file.description
|
|
18
|
+
};
|
|
19
|
+
if (file.pattern === void 0 || file.pattern === value) joinedFiles.set(literal, {
|
|
20
|
+
files: [{
|
|
21
|
+
prefix,
|
|
22
|
+
file
|
|
23
|
+
}],
|
|
24
|
+
literal: false
|
|
25
|
+
});
|
|
26
|
+
return literal;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Retrieves the originating file suggestion for a joined literal fallback.
|
|
30
|
+
* @param suggestion The suggestion to inspect.
|
|
31
|
+
* @returns Its private file metadata, if present.
|
|
32
|
+
* @internal
|
|
33
|
+
*/
|
|
34
|
+
function getJoinedFileSuggestion(suggestion) {
|
|
35
|
+
return joinedFiles.get(suggestion);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Preserves file origins when equal literal fallbacks are deduplicated.
|
|
39
|
+
* @param target The retained suggestion.
|
|
40
|
+
* @param source The duplicate suggestion.
|
|
41
|
+
* @internal
|
|
42
|
+
*/
|
|
43
|
+
function mergeJoinedFileSuggestions(target, source) {
|
|
44
|
+
const a = joinedFiles.get(target);
|
|
45
|
+
const b = joinedFiles.get(source);
|
|
46
|
+
if (a === void 0 && b === void 0) return;
|
|
47
|
+
const files = [...a?.files ?? [], ...b?.files ?? []];
|
|
48
|
+
const seen = /* @__PURE__ */ new Set();
|
|
49
|
+
joinedFiles.set(target, {
|
|
50
|
+
literal: (a?.literal ?? true) || (b?.literal ?? true),
|
|
51
|
+
files: files.filter(({ prefix, file }) => {
|
|
52
|
+
const key = JSON.stringify([
|
|
53
|
+
prefix,
|
|
54
|
+
file.type,
|
|
55
|
+
file.pattern,
|
|
56
|
+
file.includeHidden === true,
|
|
57
|
+
file.extensions?.toSorted()
|
|
58
|
+
]);
|
|
59
|
+
if (seen.has(key)) return false;
|
|
60
|
+
seen.add(key);
|
|
61
|
+
return true;
|
|
62
|
+
})
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
//#endregion
|
|
67
|
+
exports.getJoinedFileSuggestion = getJoinedFileSuggestion;
|
|
68
|
+
exports.joinedFileSuggestion = joinedFileSuggestion;
|
|
69
|
+
exports.mergeJoinedFileSuggestions = mergeJoinedFileSuggestions;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
//#region src/internal/joined-file-suggestion.ts
|
|
2
|
+
const joinedFiles = /* @__PURE__ */ new WeakMap();
|
|
3
|
+
/**
|
|
4
|
+
* Keeps a joined file fallback's origin available to the Bash encoder.
|
|
5
|
+
* Other shells retain the existing literal representation.
|
|
6
|
+
* @param prefix The option name and delimiter.
|
|
7
|
+
* @param file The original file suggestion.
|
|
8
|
+
* @param value The value prefix being completed.
|
|
9
|
+
* @returns The literal fallback with private file metadata.
|
|
10
|
+
* @internal
|
|
11
|
+
*/
|
|
12
|
+
function joinedFileSuggestion(prefix, file, value) {
|
|
13
|
+
const literal = {
|
|
14
|
+
kind: "literal",
|
|
15
|
+
text: prefix + (file.pattern ?? ""),
|
|
16
|
+
description: file.description
|
|
17
|
+
};
|
|
18
|
+
if (file.pattern === void 0 || file.pattern === value) joinedFiles.set(literal, {
|
|
19
|
+
files: [{
|
|
20
|
+
prefix,
|
|
21
|
+
file
|
|
22
|
+
}],
|
|
23
|
+
literal: false
|
|
24
|
+
});
|
|
25
|
+
return literal;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Retrieves the originating file suggestion for a joined literal fallback.
|
|
29
|
+
* @param suggestion The suggestion to inspect.
|
|
30
|
+
* @returns Its private file metadata, if present.
|
|
31
|
+
* @internal
|
|
32
|
+
*/
|
|
33
|
+
function getJoinedFileSuggestion(suggestion) {
|
|
34
|
+
return joinedFiles.get(suggestion);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Preserves file origins when equal literal fallbacks are deduplicated.
|
|
38
|
+
* @param target The retained suggestion.
|
|
39
|
+
* @param source The duplicate suggestion.
|
|
40
|
+
* @internal
|
|
41
|
+
*/
|
|
42
|
+
function mergeJoinedFileSuggestions(target, source) {
|
|
43
|
+
const a = joinedFiles.get(target);
|
|
44
|
+
const b = joinedFiles.get(source);
|
|
45
|
+
if (a === void 0 && b === void 0) return;
|
|
46
|
+
const files = [...a?.files ?? [], ...b?.files ?? []];
|
|
47
|
+
const seen = /* @__PURE__ */ new Set();
|
|
48
|
+
joinedFiles.set(target, {
|
|
49
|
+
literal: (a?.literal ?? true) || (b?.literal ?? true),
|
|
50
|
+
files: files.filter(({ prefix, file }) => {
|
|
51
|
+
const key = JSON.stringify([
|
|
52
|
+
prefix,
|
|
53
|
+
file.type,
|
|
54
|
+
file.pattern,
|
|
55
|
+
file.includeHidden === true,
|
|
56
|
+
file.extensions?.toSorted()
|
|
57
|
+
]);
|
|
58
|
+
if (seen.has(key)) return false;
|
|
59
|
+
seen.add(key);
|
|
60
|
+
return true;
|
|
61
|
+
})
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
//#endregion
|
|
66
|
+
export { getJoinedFileSuggestion, joinedFileSuggestion, mergeJoinedFileSuggestions };
|
package/dist/modifiers.cjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const require_annotations = require('./internal/annotations.cjs');
|
|
2
|
+
const require_joined_file_suggestion = require('./internal/joined-file-suggestion.cjs');
|
|
2
3
|
const require_message = require('./message.cjs');
|
|
3
4
|
const require_command_doc_metadata = require('./internal/command-doc-metadata.cjs');
|
|
4
5
|
const require_passthrough = require('./internal/passthrough.cjs');
|
|
@@ -1294,14 +1295,16 @@ function multiple(parser, options = {}) {
|
|
|
1294
1295
|
};
|
|
1295
1296
|
return require_mode_dispatch.dispatchIterableByMode(parser.mode, function* () {
|
|
1296
1297
|
const selectedValues = collectSelectedValuesSync();
|
|
1297
|
-
const emitted = /* @__PURE__ */ new
|
|
1298
|
+
const emitted = /* @__PURE__ */ new Map();
|
|
1298
1299
|
const yieldUnique = function* (suggestions) {
|
|
1299
1300
|
for (const s of suggestions) {
|
|
1300
1301
|
const key = suggestionKey(s);
|
|
1301
|
-
if (shouldInclude(selectedValues, s)
|
|
1302
|
-
|
|
1302
|
+
if (!shouldInclude(selectedValues, s)) continue;
|
|
1303
|
+
const existing = emitted.get(key);
|
|
1304
|
+
if (existing === void 0) {
|
|
1305
|
+
emitted.set(key, s);
|
|
1303
1306
|
yield s;
|
|
1304
|
-
}
|
|
1307
|
+
} else require_joined_file_suggestion.mergeJoinedFileSuggestions(existing, s);
|
|
1305
1308
|
}
|
|
1306
1309
|
};
|
|
1307
1310
|
let shouldTryFallback = false;
|
|
@@ -1314,14 +1317,16 @@ function multiple(parser, options = {}) {
|
|
|
1314
1317
|
if (shouldTryFallback) yield* yieldUnique(syncParser.suggest(require_execution_context.withChildContext(context, itemIndex, suggestFallbackState), prefix));
|
|
1315
1318
|
}, async function* () {
|
|
1316
1319
|
const selectedValues = await collectSelectedValuesAsync();
|
|
1317
|
-
const emitted = /* @__PURE__ */ new
|
|
1320
|
+
const emitted = /* @__PURE__ */ new Map();
|
|
1318
1321
|
const yieldUnique = async function* (suggestions) {
|
|
1319
1322
|
for await (const s of suggestions) {
|
|
1320
1323
|
const key = suggestionKey(s);
|
|
1321
|
-
if (shouldInclude(selectedValues, s)
|
|
1322
|
-
|
|
1324
|
+
if (!shouldInclude(selectedValues, s)) continue;
|
|
1325
|
+
const existing = emitted.get(key);
|
|
1326
|
+
if (existing === void 0) {
|
|
1327
|
+
emitted.set(key, s);
|
|
1323
1328
|
yield s;
|
|
1324
|
-
}
|
|
1329
|
+
} else require_joined_file_suggestion.mergeJoinedFileSuggestions(existing, s);
|
|
1325
1330
|
}
|
|
1326
1331
|
};
|
|
1327
1332
|
let shouldTryFallback = false;
|
package/dist/modifiers.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { annotateFreshArray, annotationKey, getAnnotations, inheritAnnotations, isInjectedAnnotationWrapper, unwrapInjectedAnnotationWrapper } from "./internal/annotations.js";
|
|
2
|
+
import { mergeJoinedFileSuggestions } from "./internal/joined-file-suggestion.js";
|
|
2
3
|
import { formatMessage, message, text } from "./message.js";
|
|
3
4
|
import { getCommandDocMetadata, withCommandDocMetadata } from "./internal/command-doc-metadata.js";
|
|
4
5
|
import { defineKnownCompletionLookup, defineOptionMatch, definePassThroughPriority, getKnownCompletion, getOptionMatch, getPassThroughPriority } from "./internal/passthrough.js";
|
|
@@ -1294,14 +1295,16 @@ function multiple(parser, options = {}) {
|
|
|
1294
1295
|
};
|
|
1295
1296
|
return dispatchIterableByMode(parser.mode, function* () {
|
|
1296
1297
|
const selectedValues = collectSelectedValuesSync();
|
|
1297
|
-
const emitted = /* @__PURE__ */ new
|
|
1298
|
+
const emitted = /* @__PURE__ */ new Map();
|
|
1298
1299
|
const yieldUnique = function* (suggestions) {
|
|
1299
1300
|
for (const s of suggestions) {
|
|
1300
1301
|
const key = suggestionKey(s);
|
|
1301
|
-
if (shouldInclude(selectedValues, s)
|
|
1302
|
-
|
|
1302
|
+
if (!shouldInclude(selectedValues, s)) continue;
|
|
1303
|
+
const existing = emitted.get(key);
|
|
1304
|
+
if (existing === void 0) {
|
|
1305
|
+
emitted.set(key, s);
|
|
1303
1306
|
yield s;
|
|
1304
|
-
}
|
|
1307
|
+
} else mergeJoinedFileSuggestions(existing, s);
|
|
1305
1308
|
}
|
|
1306
1309
|
};
|
|
1307
1310
|
let shouldTryFallback = false;
|
|
@@ -1314,14 +1317,16 @@ function multiple(parser, options = {}) {
|
|
|
1314
1317
|
if (shouldTryFallback) yield* yieldUnique(syncParser.suggest(withChildContext(context, itemIndex, suggestFallbackState), prefix));
|
|
1315
1318
|
}, async function* () {
|
|
1316
1319
|
const selectedValues = await collectSelectedValuesAsync();
|
|
1317
|
-
const emitted = /* @__PURE__ */ new
|
|
1320
|
+
const emitted = /* @__PURE__ */ new Map();
|
|
1318
1321
|
const yieldUnique = async function* (suggestions) {
|
|
1319
1322
|
for await (const s of suggestions) {
|
|
1320
1323
|
const key = suggestionKey(s);
|
|
1321
|
-
if (shouldInclude(selectedValues, s)
|
|
1322
|
-
|
|
1324
|
+
if (!shouldInclude(selectedValues, s)) continue;
|
|
1325
|
+
const existing = emitted.get(key);
|
|
1326
|
+
if (existing === void 0) {
|
|
1327
|
+
emitted.set(key, s);
|
|
1323
1328
|
yield s;
|
|
1324
|
-
}
|
|
1329
|
+
} else mergeJoinedFileSuggestions(existing, s);
|
|
1325
1330
|
}
|
|
1326
1331
|
};
|
|
1327
1332
|
let shouldTryFallback = false;
|