@optique/core 1.0.15-dev.2812 → 1.0.15-dev.2816
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 +352 -38
- package/dist/completion.js +352 -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.cjs
CHANGED
|
@@ -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
|
-
|
|
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 = 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";
|
|
@@ -946,8 +1186,10 @@ def args-split []: string -> list<string> {
|
|
|
946
1186
|
mut current_token = ""
|
|
947
1187
|
mut result: list<string> = []
|
|
948
1188
|
mut prev_state = $STATE_NORMAL
|
|
1189
|
+
mut ends_with_delimiter = false
|
|
949
1190
|
|
|
950
1191
|
for char in ($in | split chars) {
|
|
1192
|
+
$ends_with_delimiter = false
|
|
951
1193
|
if $state == $STATE_ESCAPE {
|
|
952
1194
|
$current_token = $current_token + $char
|
|
953
1195
|
$state = $prev_state
|
|
@@ -960,6 +1202,7 @@ def args-split []: string -> list<string> {
|
|
|
960
1202
|
} else if $char == '"' {
|
|
961
1203
|
$state = $STATE_IN_DOUBLE_QUOTE
|
|
962
1204
|
} else if ($char in $WHITESPACES) {
|
|
1205
|
+
$ends_with_delimiter = true
|
|
963
1206
|
if ($current_token | is-not-empty) {
|
|
964
1207
|
$result = $result | append $current_token
|
|
965
1208
|
$current_token = ""
|
|
@@ -981,12 +1224,52 @@ def args-split []: string -> list<string> {
|
|
|
981
1224
|
}
|
|
982
1225
|
}
|
|
983
1226
|
}
|
|
984
|
-
|
|
1227
|
+
# A quote left open at the end starts the argument being completed, even
|
|
1228
|
+
# when nothing follows it yet
|
|
1229
|
+
if ($current_token | is-not-empty) or ($state in [$STATE_IN_SINGLE_QUOTE $STATE_IN_DOUBLE_QUOTE]) {
|
|
985
1230
|
$result = $result | append $current_token
|
|
986
1231
|
}
|
|
1232
|
+
# Whitespace outside quotes at the end starts a new, still empty argument
|
|
1233
|
+
if $ends_with_delimiter {
|
|
1234
|
+
$result = $result | append ''
|
|
1235
|
+
}
|
|
987
1236
|
$result
|
|
988
1237
|
}
|
|
989
1238
|
|
|
1239
|
+
# Helper to tell whether Nushell would not read a value back verbatim as a
|
|
1240
|
+
# single argument. A leading ! is included because the line editor expands
|
|
1241
|
+
# history (e.g., !!) there. With --allow-expansion, a leading tilde and n-dot
|
|
1242
|
+
# path segments (e.g., ...) are allowed to expand.
|
|
1243
|
+
def needs-quote [--allow-expansion]: string -> bool {
|
|
1244
|
+
let value = $in
|
|
1245
|
+
let special = ["'" '"' '\`' "#" "(" ")" "[" "]" "{" "}" "|" ";" "&" "$" "*" "?" "<" ">"]
|
|
1246
|
+
(
|
|
1247
|
+
($value =~ '\\s')
|
|
1248
|
+
or ($special | any {|c| $value | str contains $c })
|
|
1249
|
+
or ($value =~ '^(\\+\\+|[-+*/])?=$')
|
|
1250
|
+
or ($value | str starts-with '!')
|
|
1251
|
+
or ((not $allow_expansion) and (
|
|
1252
|
+
($value | str starts-with '~')
|
|
1253
|
+
or ($value | split row -r '[/\\\\]' | any {|s| $s =~ '^\\.{3,}$' })
|
|
1254
|
+
))
|
|
1255
|
+
)
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
# Helper to quote a value so that Nushell reads it back verbatim as a
|
|
1259
|
+
# single argument. The line editor expands history at a ! that follows
|
|
1260
|
+
# whitespace even inside quotes, so such a ! is put between two quoted parts,
|
|
1261
|
+
# right after a quote, where it is left alone.
|
|
1262
|
+
def quote-arg []: string -> string {
|
|
1263
|
+
let value = $in
|
|
1264
|
+
if ($value | is-empty) or not ($value | needs-quote) {
|
|
1265
|
+
$value
|
|
1266
|
+
} else if ($value | str contains "'") {
|
|
1267
|
+
'"' + ($value | str replace -a '\\' '\\\\' | str replace -a '"' '\\"' | str replace -ar '(\\s)!' '\${1}"!"') + '"'
|
|
1268
|
+
} else {
|
|
1269
|
+
"'" + ($value | str replace -ar '(\\s)!' "\${1}'!'") + "'"
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
|
|
990
1273
|
# Completion command that calls back to the CLI
|
|
991
1274
|
export def "${functionName}" [context: string] {
|
|
992
1275
|
# Split context into tokens, handling quotes properly
|
|
@@ -999,18 +1282,11 @@ export def "${functionName}" [context: string] {
|
|
|
999
1282
|
[]
|
|
1000
1283
|
}
|
|
1001
1284
|
|
|
1002
|
-
# If context ends with whitespace, add empty string to represent the new argument
|
|
1003
|
-
let args_with_empty = if ($context | str ends-with ' ') {
|
|
1004
|
-
$args | append ''
|
|
1005
|
-
} else {
|
|
1006
|
-
$args
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
1285
|
# Ensure at least one argument (empty string) to get completions not script
|
|
1010
|
-
let final_args = if ($
|
|
1286
|
+
let final_args = if ($args | is-empty) {
|
|
1011
1287
|
['']
|
|
1012
1288
|
} else {
|
|
1013
|
-
$
|
|
1289
|
+
$args
|
|
1014
1290
|
}
|
|
1015
1291
|
|
|
1016
1292
|
let output = try {
|
|
@@ -1192,19 +1468,45 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
|
|
|
1192
1468
|
if ($parsed.parent | is-empty) { "" } else if ($parsed.parent | str ends-with "/") { $parsed.parent } else { $parsed.parent + "/" }
|
|
1193
1469
|
}
|
|
1194
1470
|
|
|
1195
|
-
# Format file completions
|
|
1471
|
+
# Format file completions, quoting names that Nushell would not read
|
|
1472
|
+
# back as a single argument
|
|
1196
1473
|
$filtered | each {|item|
|
|
1197
|
-
let
|
|
1198
|
-
|
|
1474
|
+
let base = if $item.type == dir {
|
|
1475
|
+
($item.name | path basename) + "/"
|
|
1476
|
+
} else {
|
|
1477
|
+
$item.name | path basename
|
|
1478
|
+
}
|
|
1479
|
+
let name = $dir_prefix + $base
|
|
1480
|
+
# Nushell expands a leading tilde even in a file name, but not after ./
|
|
1481
|
+
let spelled = if ($dir_prefix | is-empty) and ($base | str starts-with '~') {
|
|
1482
|
+
'./' + $base
|
|
1483
|
+
} else {
|
|
1484
|
+
$name
|
|
1485
|
+
}
|
|
1486
|
+
# A tilde or n-dot prefix typed by the user is meant to be expanded
|
|
1487
|
+
# by Nushell, which quoting would prevent, so expand such a prefix
|
|
1488
|
+
# here when the value has to be quoted
|
|
1489
|
+
let literal_ndots = ($base | str replace -r '[/\\\\]$' '') =~ '^\\.{3,}$'
|
|
1490
|
+
let expandable = ($dir_prefix | str starts-with '~') or ($dir_prefix | split row -r '[/\\\\]' | any {|s| $s =~ '^\\.{3,}$' })
|
|
1491
|
+
let value = if not ($literal_ndots or ($spelled | needs-quote --allow-expansion)) {
|
|
1492
|
+
$spelled
|
|
1493
|
+
} else if $expandable {
|
|
1494
|
+
let expanded_prefix = ($dir_prefix | path expand --no-symlink)
|
|
1495
|
+
let expanded_prefix = if ($expanded_prefix | str ends-with "/") { $expanded_prefix } else { $expanded_prefix + "/" }
|
|
1496
|
+
($expanded_prefix + $base) | quote-arg
|
|
1199
1497
|
} else {
|
|
1200
|
-
$
|
|
1498
|
+
$spelled | quote-arg
|
|
1499
|
+
}
|
|
1500
|
+
if $value == $name {
|
|
1501
|
+
{ value: $value }
|
|
1502
|
+
} else {
|
|
1503
|
+
{ value: $value, display_override: $name }
|
|
1201
1504
|
}
|
|
1202
|
-
{ value: $name }
|
|
1203
1505
|
}
|
|
1204
1506
|
} else {
|
|
1205
1507
|
# Regular literal completion - split by tab
|
|
1206
1508
|
let parts = ($line | split row "\t")
|
|
1207
|
-
if ($parts | length) >= 2 {
|
|
1509
|
+
let record = if ($parts | length) >= 2 {
|
|
1208
1510
|
# value\\tdescription format
|
|
1209
1511
|
{ value: ($parts | get 0), description: ($parts | get 1) }
|
|
1210
1512
|
} else if ($parts | length) == 1 and ($parts | get 0 | is-not-empty) {
|
|
@@ -1213,6 +1515,18 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
|
|
|
1213
1515
|
} else {
|
|
1214
1516
|
null
|
|
1215
1517
|
}
|
|
1518
|
+
# Quote values that Nushell would not read back as a single argument,
|
|
1519
|
+
# while the menu keeps showing them as they are
|
|
1520
|
+
if $record == null {
|
|
1521
|
+
null
|
|
1522
|
+
} else {
|
|
1523
|
+
let value = ($record.value | quote-arg)
|
|
1524
|
+
if $value == $record.value {
|
|
1525
|
+
$record
|
|
1526
|
+
} else {
|
|
1527
|
+
$record | upsert value $value | insert display_override $record.value
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1216
1530
|
}
|
|
1217
1531
|
} | flatten | compact
|
|
1218
1532
|
}
|