@optique/core 1.3.5 → 1.3.6
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 +1709 -230
- package/dist/completion.js +1709 -230
- package/dist/constructs.cjs +7 -1
- package/dist/constructs.d.cts +2 -0
- package/dist/constructs.d.ts +2 -0
- package/dist/constructs.js +7 -1
- package/dist/internal/joined-file-suggestion.cjs +73 -0
- package/dist/internal/joined-file-suggestion.js +70 -0
- package/dist/internal/parser.d.cts +8 -0
- package/dist/internal/parser.d.ts +8 -0
- package/dist/modifiers.cjs +14 -8
- package/dist/modifiers.js +14 -8
- package/dist/primitives.cjs +19 -22
- package/dist/primitives.js +19 -22
- package/dist/suggestion.cjs +12 -5
- package/dist/suggestion.d.cts +1 -1
- package/dist/suggestion.d.ts +1 -1
- package/dist/suggestion.js +12 -5
- package/dist/valueparser.cjs +2 -1
- package/dist/valueparser.js +2 -1
- package/package.json +2 -2
package/dist/completion.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { formatMessage } from "./message.js";
|
|
2
|
+
import { getJoinedFileSuggestion } from "./internal/joined-file-suggestion.js";
|
|
2
3
|
|
|
3
4
|
//#region src/completion.ts
|
|
4
5
|
/**
|
|
@@ -40,6 +41,17 @@ function validateProgramName(programName) {
|
|
|
40
41
|
function encodePattern(pattern) {
|
|
41
42
|
return pattern.replace(/%/g, "%25").replace(/:/g, "%3A");
|
|
42
43
|
}
|
|
44
|
+
/** Encodes optional literal insertion metadata without changing old directives. */
|
|
45
|
+
function encodeFilePrefix(suggestion) {
|
|
46
|
+
const prefix = suggestion.prefix ?? "";
|
|
47
|
+
if (prefix === "" || !(suggestion.pattern ?? "").startsWith(prefix) || sanitizeForTransport(prefix) !== prefix) return "";
|
|
48
|
+
return `:${encodePattern(prefix)}`;
|
|
49
|
+
}
|
|
50
|
+
/** Escapes literal records that could otherwise be read as Bash directives. */
|
|
51
|
+
function encodeBashLiteral(value) {
|
|
52
|
+
const text = sanitizeForTransport(value);
|
|
53
|
+
return /^(?:__FILE__|__JOINED_FILE__|__LITERAL__):/.test(text) ? `__LITERAL__:${text}` : text;
|
|
54
|
+
}
|
|
43
55
|
function encodeExtensions(extensions) {
|
|
44
56
|
return extensions?.map((ext) => ext.replace(/^\./, "")).join(",") ?? "";
|
|
45
57
|
}
|
|
@@ -56,6 +68,17 @@ function sanitizeForTransport(text) {
|
|
|
56
68
|
return text.replace(/[\t\n\r\0]/g, " ");
|
|
57
69
|
}
|
|
58
70
|
/**
|
|
71
|
+
* Escapes literal values that would otherwise be interpreted as transport
|
|
72
|
+
* directives. Escape the escape prefix itself so decoding is unambiguous.
|
|
73
|
+
* @param text The literal suggestion text.
|
|
74
|
+
* @returns The sanitized and, if necessary, escaped literal text.
|
|
75
|
+
* @internal
|
|
76
|
+
*/
|
|
77
|
+
function encodeLiteral(text) {
|
|
78
|
+
const sanitized = sanitizeForTransport(text);
|
|
79
|
+
return sanitized.startsWith("__FILE__:") || sanitized.startsWith("__LITERAL__:") ? `__LITERAL__:${sanitized}` : sanitized;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
59
82
|
* The Bash shell completion generator.
|
|
60
83
|
* @since 0.6.0
|
|
61
84
|
*/
|
|
@@ -67,14 +90,297 @@ const bash = {
|
|
|
67
90
|
return `
|
|
68
91
|
function _${programName} () {
|
|
69
92
|
COMPREPLY=()
|
|
70
|
-
local
|
|
93
|
+
local __raw_current="\${COMP_WORDS[COMP_CWORD]}"
|
|
94
|
+
local current="" __quote="" __char __next __index __quote_offset=0
|
|
95
|
+
local __file_mode=0
|
|
96
|
+
if [[ $(type -t compopt) == builtin ]]; then
|
|
97
|
+
compopt +o filenames 2>/dev/null || true
|
|
98
|
+
fi
|
|
99
|
+
# Dequote the shell word without evaluating filename contents. Readline
|
|
100
|
+
# returns escaped words after Tab, and users may start with an open quote.
|
|
101
|
+
for ((__index=0; __index<\${#__raw_current}; __index++)); do
|
|
102
|
+
__char="\${__raw_current:__index:1}"
|
|
103
|
+
if [[ -z "$__quote" ]]; then
|
|
104
|
+
case "$__char" in
|
|
105
|
+
\\\\) ((++__index)); current+="\${__raw_current:__index:1}" ;;
|
|
106
|
+
\\'|\\") __quote="$__char"; __quote_offset=\${#current} ;;
|
|
107
|
+
*) current+="$__char" ;;
|
|
108
|
+
esac
|
|
109
|
+
elif [[ "$__quote" == "'" ]]; then
|
|
110
|
+
if [[ "$__char" == "'" ]]; then
|
|
111
|
+
__quote=""
|
|
112
|
+
else
|
|
113
|
+
current+="$__char"
|
|
114
|
+
fi
|
|
115
|
+
else
|
|
116
|
+
case "$__char" in
|
|
117
|
+
\\") __quote="" ;;
|
|
118
|
+
\\\\)
|
|
119
|
+
__next="\${__raw_current:__index+1:1}"
|
|
120
|
+
case "$__next" in
|
|
121
|
+
\\$|\\\`|\\"|\\\\)
|
|
122
|
+
((++__index)); current+="$__next" ;;
|
|
123
|
+
*) current+="$__char" ;;
|
|
124
|
+
esac ;;
|
|
125
|
+
*) current+="$__char" ;;
|
|
126
|
+
esac
|
|
127
|
+
fi
|
|
128
|
+
done
|
|
129
|
+
local __tilde_pattern='^(~[a-zA-Z0-9_.+-]*)(/.*)?$'
|
|
130
|
+
local __tilde_glob_pattern='^(~[a-zA-Z0-9_.+-]*)(/.*)?$'
|
|
71
131
|
local prev=("\${COMP_WORDS[@]:1:COMP_CWORD-1}")
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
132
|
+
# Bash splits unquoted '=' at its default word breaks. Reassemble only
|
|
133
|
+
# adjacent fragments, including further word breaks inside a joined value,
|
|
134
|
+
# leaving whitespace-separated arguments intact.
|
|
135
|
+
local __plain_current="$current" __remove="" __word __joined=0
|
|
136
|
+
local __raw_joined_current="$__raw_current"
|
|
137
|
+
local -a __raw_offsets=() __raw_quotes=()
|
|
138
|
+
if [[ \${COMP_LINE+x} ]]; then
|
|
139
|
+
local -a __words=()
|
|
140
|
+
local __rest="$COMP_LINE" __previous="" __space __valid=1 __i __j=0
|
|
141
|
+
local __shell_quote="" __shell_escape=0 __boundary=0 __previous_boundary=0 __scan __symbol __shell_dollar=0
|
|
142
|
+
# Reconstruct the prefix at the cursor, leaving any suffix to Readline.
|
|
143
|
+
if [[ \${COMP_POINT+x} ]]; then
|
|
144
|
+
__rest="\${COMP_LINE:0:COMP_POINT}"
|
|
145
|
+
fi
|
|
146
|
+
for ((__i=0; __i<=COMP_CWORD; __i++)); do
|
|
147
|
+
__word="\${COMP_WORDS[__i]}"
|
|
148
|
+
__space=0
|
|
149
|
+
while [[ "$__rest" == [[:space:]]* ]]; do
|
|
150
|
+
__space=1
|
|
151
|
+
__rest="\${__rest:1}"
|
|
152
|
+
done
|
|
153
|
+
if (( __i == COMP_CWORD && \${#__rest} < \${#__word} )); then
|
|
154
|
+
__word="\${__word:0:\${#__rest}}"
|
|
155
|
+
fi
|
|
156
|
+
if [[ "\${__rest:0:\${#__word}}" != "$__word" ]]; then
|
|
157
|
+
__valid=0
|
|
158
|
+
break
|
|
159
|
+
fi
|
|
160
|
+
# Quoted or escaped operators belong to words; bare operators separate
|
|
161
|
+
# shell syntax even when no whitespace surrounds a redirection.
|
|
162
|
+
__boundary=0
|
|
163
|
+
for ((__scan=0; __scan<\${#__word}; __scan++)); do
|
|
164
|
+
__symbol="\${__word:__scan:1}"
|
|
165
|
+
if (( __shell_escape )); then
|
|
166
|
+
__shell_escape=0
|
|
167
|
+
elif [[ "$__shell_quote" == ansi ]]; then
|
|
168
|
+
if [[ "$__symbol" == '\\' ]]; then __shell_escape=1
|
|
169
|
+
elif [[ "$__symbol" == "'" ]]; then __shell_quote=""; fi
|
|
170
|
+
elif [[ "$__shell_quote" == "'" ]]; then
|
|
171
|
+
[[ "$__symbol" == "'" ]] && __shell_quote=""
|
|
172
|
+
elif [[ "$__symbol" == '\\' ]]; then
|
|
173
|
+
__shell_escape=1
|
|
174
|
+
elif [[ -n "$__shell_quote" ]]; then
|
|
175
|
+
[[ "$__symbol" == "$__shell_quote" ]] && __shell_quote=""
|
|
176
|
+
else
|
|
177
|
+
case "$__symbol" in
|
|
178
|
+
"'")
|
|
179
|
+
if (( __shell_dollar )); then __shell_quote=ansi
|
|
180
|
+
else __shell_quote="'"; fi ;;
|
|
181
|
+
'"') __shell_quote='"' ;;
|
|
182
|
+
'$') __shell_dollar=1 ;;
|
|
183
|
+
'>'|'<'|'|'|'&'|';'|'('|')') __boundary=1 ;;
|
|
184
|
+
esac
|
|
185
|
+
fi
|
|
186
|
+
[[ "$__symbol" == '$' && -z "$__shell_quote" ]] || __shell_dollar=0
|
|
187
|
+
done
|
|
188
|
+
if (( __i > 1 && !__space && !__boundary && !__previous_boundary )) &&
|
|
189
|
+
{ [[ -n "$__word" && -z "\${__word//=/}" ]] ||
|
|
190
|
+
[[ -n "$__previous" && -z "\${__previous//=/}" ]] ||
|
|
191
|
+
[[ "\${__words[__j-1]}" == [-/+]* ]] ||
|
|
192
|
+
[[ "$__word" == =* && "\${__words[__j-1]}" == [-/+]* ]]; }; then
|
|
193
|
+
__words[__j-1]+="$__word"
|
|
194
|
+
else
|
|
195
|
+
__words[__j]="$__word"
|
|
196
|
+
((++__j))
|
|
197
|
+
fi
|
|
198
|
+
__rest="\${__rest:\${#__word}}"
|
|
199
|
+
__previous="$__word"
|
|
200
|
+
__previous_boundary=$__boundary
|
|
201
|
+
done
|
|
202
|
+
if (( __valid )); then
|
|
203
|
+
current="\${__words[__j-1]}"
|
|
204
|
+
__raw_joined_current="$current"
|
|
205
|
+
prev=("\${__words[@]:1:__j-2}")
|
|
206
|
+
# Bash passes the fragment Readline will replace as the second argument,
|
|
207
|
+
# which is empty when the cursor follows a word-break character.
|
|
208
|
+
if [[ \${2+x} ]]; then
|
|
209
|
+
__remove="\${current:0:\${#current}-\${#2}}"
|
|
210
|
+
elif [[ -n "$__raw_current" && -z "\${__raw_current//=/}" ]]; then
|
|
211
|
+
__remove="$current"
|
|
212
|
+
else
|
|
213
|
+
__remove="\${current:0:\${#current}-\${#__raw_current}}"
|
|
214
|
+
fi
|
|
215
|
+
# Decode joined shell words without evaluating substitutions. Keep quote
|
|
216
|
+
# state across the reassembled fragments and decode the preserved prefix
|
|
217
|
+
# separately so trimming uses the same representation as suggestions.
|
|
218
|
+
__words[__j]="$__remove"
|
|
219
|
+
local __decoded __decoded_option=0 __before_quote __join_quote __char __next __k __ansi __part __digits __code __join_quote_offset
|
|
220
|
+
for ((__i=1; __i<=__j; __i++)); do
|
|
221
|
+
__word="\${__words[__i]}"
|
|
222
|
+
__decoded=""; __join_quote=""; __join_quote_offset=0
|
|
223
|
+
for ((__k=0; __k<\${#__word}; __k++)); do
|
|
224
|
+
__char="\${__word:__k:1}"
|
|
225
|
+
__before_quote="$__join_quote"
|
|
226
|
+
if [[ -z "$__join_quote" ]]; then
|
|
227
|
+
case "$__char" in
|
|
228
|
+
'\\') ((++__k)); __decoded+="\${__word:__k:1}" ;;
|
|
229
|
+
'$')
|
|
230
|
+
if [[ "\${__word:__k+1:1}" == "'" ]]; then
|
|
231
|
+
__join_quote=ansi; __join_quote_offset=\${#__decoded}; __ansi=""; ((++__k))
|
|
232
|
+
elif [[ "\${__word:__k+1:1}" == '"' ]]; then
|
|
233
|
+
__join_quote='"'; __join_quote_offset=\${#__decoded}; ((++__k))
|
|
234
|
+
else __decoded+="$__char"; fi ;;
|
|
235
|
+
"'"|'"') __join_quote="$__char"; __join_quote_offset=\${#__decoded} ;;
|
|
236
|
+
*) __decoded+="$__char" ;;
|
|
237
|
+
esac
|
|
238
|
+
elif [[ "$__join_quote" == ansi ]]; then
|
|
239
|
+
if [[ "$__char" == "'" ]]; then
|
|
240
|
+
printf -v __part '%b' "$__ansi"
|
|
241
|
+
__decoded+="$__part"; __join_quote=""
|
|
242
|
+
elif [[ "$__char" == '\\' ]]; then
|
|
243
|
+
((++__k)); __next="\${__word:__k:1}"
|
|
244
|
+
case "$__next" in
|
|
245
|
+
[0-7])
|
|
246
|
+
__digits="$__next"
|
|
247
|
+
while (( \${#__digits} < 3 )) && [[ "\${__word:__k+1:1}" == [0-7] ]]; do
|
|
248
|
+
((++__k)); __digits+="\${__word:__k:1}"
|
|
249
|
+
done
|
|
250
|
+
__ansi+='\\0'"$__digits" ;;
|
|
251
|
+
c)
|
|
252
|
+
__next="\${__word:__k+1:1}"
|
|
253
|
+
if [[ -n "$__next" && "$__next" != "'" ]]; then
|
|
254
|
+
((++__k)); printf -v __code '%d' "'$__next"
|
|
255
|
+
if [[ "$__next" == '?' ]]; then __code=127
|
|
256
|
+
else __code=$((__code & 31)); fi
|
|
257
|
+
printf -v __part '\\\\%03o' "$__code"
|
|
258
|
+
__ansi+="$__part"
|
|
259
|
+
else __ansi+='\\\\c'; fi ;;
|
|
260
|
+
"'"|'"'|'?') __ansi+="$__next" ;;
|
|
261
|
+
*) __ansi+='\\'"$__next" ;;
|
|
262
|
+
esac
|
|
263
|
+
else __ansi+="$__char"; fi
|
|
264
|
+
elif [[ "$__join_quote" == "'" ]]; then
|
|
265
|
+
if [[ "$__char" == "'" ]]; then __join_quote=""
|
|
266
|
+
else __decoded+="$__char"; fi
|
|
267
|
+
else
|
|
268
|
+
case "$__char" in
|
|
269
|
+
'"') __join_quote="" ;;
|
|
270
|
+
'\\')
|
|
271
|
+
__next="\${__word:__k+1:1}"
|
|
272
|
+
case "$__next" in
|
|
273
|
+
'$'|'\`'|'"'|'\\') ((++__k)); __decoded+="$__next" ;;
|
|
274
|
+
*) __decoded+="$__char" ;;
|
|
275
|
+
esac ;;
|
|
276
|
+
*) __decoded+="$__char" ;;
|
|
277
|
+
esac
|
|
278
|
+
fi
|
|
279
|
+
if (( __i == __j - 1 )) &&
|
|
280
|
+
{ [[ ! \${__raw_offsets[\${#__decoded}]+x} ]] ||
|
|
281
|
+
[[ -n "$__before_quote" && -z "$__join_quote" ]]; }; then
|
|
282
|
+
__raw_offsets[\${#__decoded}]=$((__k + 1))
|
|
283
|
+
__raw_quotes[\${#__decoded}]="$__join_quote"
|
|
284
|
+
fi
|
|
285
|
+
done
|
|
286
|
+
if [[ "$__join_quote" == ansi ]]; then
|
|
287
|
+
printf -v __part '%b' "$__ansi"
|
|
288
|
+
__decoded+="$__part"
|
|
289
|
+
fi
|
|
290
|
+
# Shell syntax may precede the option prefix. Decide from decoded text,
|
|
291
|
+
# retaining the existing native-file handling for non-option words.
|
|
292
|
+
if [[ "$__decoded" == [-/+]* ]] || (( __i == __j && __decoded_option )); then
|
|
293
|
+
if (( __i == __j - 1 )); then
|
|
294
|
+
__decoded_option=1
|
|
295
|
+
__quote="$__join_quote"
|
|
296
|
+
[[ "$__quote" == ansi ]] && __quote="'"
|
|
297
|
+
__quote_offset=$__join_quote_offset
|
|
298
|
+
fi
|
|
299
|
+
__words[__i]="$__decoded"
|
|
300
|
+
fi
|
|
301
|
+
done
|
|
302
|
+
if [[ "\${__words[__j-1]}" == -*=* || "\${__words[__j-1]}" == /*:* || "\${__words[__j-1]}" != "$__raw_current" ]]; then
|
|
303
|
+
current="\${__words[__j-1]}"
|
|
304
|
+
__joined=1
|
|
305
|
+
else
|
|
306
|
+
current="$__plain_current"
|
|
307
|
+
__words[__j]=""
|
|
308
|
+
fi
|
|
309
|
+
prev=("\${__words[@]:1:__j-2}")
|
|
310
|
+
__remove="\${__words[__j]}"
|
|
311
|
+
fi
|
|
312
|
+
fi
|
|
313
|
+
|
|
314
|
+
# Joined file directives retain the exact option prefix and metadata chosen
|
|
315
|
+
# by the parser; do not infer option boundaries through speculative retries.
|
|
316
|
+
local __protocol_current="$__raw_current"
|
|
317
|
+
(( __joined )) && __protocol_current="$current"
|
|
318
|
+
local -a __lines=() __replies=()
|
|
319
|
+
local line __reply_prefix="" __file_current="$current" __literal=0
|
|
320
|
+
while IFS= read -r line || [[ -n "$line" ]]; do
|
|
321
|
+
[[ -n "$line" ]] && __lines+=("$line")
|
|
322
|
+
done < <(${programName} ${escapedArgs} "\${prev[@]}" "$__protocol_current" 2>/dev/null)
|
|
323
|
+
for line in "\${__lines[@]}"; do
|
|
324
|
+
COMPREPLY=()
|
|
325
|
+
__reply_prefix=""
|
|
326
|
+
__file_current="$current"
|
|
327
|
+
__literal=0
|
|
328
|
+
# Protocol tags are case-sensitive even when nocasematch is enabled.
|
|
329
|
+
if [ "\${line:0:12}" = '__LITERAL__:' ]; then
|
|
330
|
+
line="\${line#__LITERAL__:}"
|
|
331
|
+
__literal=1
|
|
332
|
+
elif [ "\${line:0:16}" = '__JOINED_FILE__:' ]; then
|
|
333
|
+
line="\${line#__JOINED_FILE__:}"
|
|
334
|
+
__reply_prefix="\${line%%:*}"
|
|
335
|
+
__reply_prefix="\${__reply_prefix//%3A/:}"
|
|
336
|
+
__reply_prefix="\${__reply_prefix//%25/%}"
|
|
337
|
+
line="__FILE__:\${line#*:}"
|
|
338
|
+
__file_current="\${current:\${#__reply_prefix}}"
|
|
339
|
+
fi
|
|
340
|
+
local __raw_file_current="$__raw_current" __file_quote_offset=$__quote_offset __file_tilde_quoted=0
|
|
341
|
+
if [[ -n "$__reply_prefix" ]]; then
|
|
342
|
+
local __raw_offset="\${__raw_offsets[\${#__reply_prefix}]:-\${#__reply_prefix}}"
|
|
343
|
+
__raw_file_current="\${__raw_joined_current:__raw_offset}"
|
|
344
|
+
# Quoting may start before the option name and remain active at the
|
|
345
|
+
# delimiter. Keep that context when deciding whether tilde is literal.
|
|
346
|
+
if [[ -n "\${__raw_quotes[\${#__reply_prefix}]-}" ]]; then
|
|
347
|
+
__file_tilde_quoted=1
|
|
348
|
+
fi
|
|
349
|
+
if (( __file_quote_offset >= \${#__reply_prefix} )); then
|
|
350
|
+
__file_quote_offset=$((__file_quote_offset - \${#__reply_prefix}))
|
|
351
|
+
fi
|
|
352
|
+
fi
|
|
353
|
+
if [ "$__literal" = 0 ] && [ "\${line:0:9}" = '__FILE__:' ]; then
|
|
354
|
+
# Parse file completion directive:
|
|
355
|
+
# __FILE__:type:extensions:pattern:hidden[:insertion_prefix]
|
|
356
|
+
local insertion_prefix="" __key_current="$__file_current"
|
|
357
|
+
IFS=':' read -r _ type extensions pattern hidden insertion_prefix <<< "$line"
|
|
76
358
|
extensions="\${extensions//,\\./,}"; extensions="\${extensions#.}"
|
|
77
359
|
pattern="\${pattern//%3A/:}"; pattern="\${pattern//%25/%}"
|
|
360
|
+
insertion_prefix="\${insertion_prefix//%3A/:}"; insertion_prefix="\${insertion_prefix//%25/%}"
|
|
361
|
+
local __reply_start=\${#COMPREPLY[@]}
|
|
362
|
+
|
|
363
|
+
# Inserted keys are not filesystem paths. Match only the value part,
|
|
364
|
+
# then restore the key once the file candidates have been filtered.
|
|
365
|
+
if [[ -n "$insertion_prefix" ]] &&
|
|
366
|
+
[ "\${pattern:0:\${#insertion_prefix}}" = "$insertion_prefix" ] &&
|
|
367
|
+
[ "\${__file_current:0:\${#insertion_prefix}}" = "$insertion_prefix" ]; then
|
|
368
|
+
pattern="\${pattern:\${#insertion_prefix}}"
|
|
369
|
+
__file_current="\${__file_current:\${#insertion_prefix}}"
|
|
370
|
+
if [ "\${__raw_file_current:0:\${#insertion_prefix}}" = "$insertion_prefix" ]; then
|
|
371
|
+
__raw_file_current="\${__raw_file_current:\${#insertion_prefix}}"
|
|
372
|
+
fi
|
|
373
|
+
else
|
|
374
|
+
insertion_prefix=""
|
|
375
|
+
fi
|
|
376
|
+
|
|
377
|
+
# path() echoes the raw shell word as its pattern. Once dequoted,
|
|
378
|
+
# use the literal current-word prefix instead of the escaped pattern.
|
|
379
|
+
if [[ "$pattern" == "$__raw_file_current" && "$__file_current" != "$__raw_file_current" ]] ||
|
|
380
|
+
{ [[ -n "$__reply_prefix" ]] && [ "$pattern" = "$__file_current" ] &&
|
|
381
|
+
{ [[ "$pattern" != *[\*\?]* ]] || [ "$__raw_file_current" != "$__file_current" ]; }; }; then
|
|
382
|
+
pattern=""
|
|
383
|
+
fi
|
|
78
384
|
|
|
79
385
|
# Save and adjust glob/shell options for safe file completion
|
|
80
386
|
local __dotglob_was_set=0 __failglob_was_set=0 __noglob_was_set=0
|
|
@@ -90,12 +396,12 @@ function _${programName} () {
|
|
|
90
396
|
unset GLOBIGNORE
|
|
91
397
|
|
|
92
398
|
# Expand tilde prefix for file globbing
|
|
93
|
-
local __glob_current="$
|
|
94
|
-
if [[ "$
|
|
399
|
+
local __glob_current="$__file_current" __tilde_prefix="" __tilde_expanded=""
|
|
400
|
+
if [[ $__file_tilde_quoted == 0 && "$__raw_file_current" == "~"* && "$__file_current" =~ $__tilde_pattern ]]; then
|
|
95
401
|
__tilde_prefix="\${BASH_REMATCH[1]}"
|
|
96
402
|
eval "__tilde_expanded=\$__tilde_prefix" 2>/dev/null || true
|
|
97
403
|
if [[ -n "$__tilde_expanded" && "$__tilde_expanded" != "$__tilde_prefix" ]]; then
|
|
98
|
-
__glob_current="\${__tilde_expanded}\${
|
|
404
|
+
__glob_current="\${__tilde_expanded}\${__file_current#\$__tilde_prefix}"
|
|
99
405
|
else
|
|
100
406
|
__tilde_prefix=""
|
|
101
407
|
fi
|
|
@@ -106,10 +412,11 @@ function _${programName} () {
|
|
|
106
412
|
# However, if the user has already typed beyond the pattern (e.g.,
|
|
107
413
|
# pattern="src/" and current="src/ma"), preserve the typed suffix
|
|
108
414
|
# for incremental filtering.
|
|
109
|
-
local __from_pattern=0
|
|
415
|
+
local __from_pattern=0 __filter_pattern=0 __pattern_prefix=""
|
|
416
|
+
local __pattern_tilde_prefix="" __pattern_tilde_expanded=""
|
|
110
417
|
if [[ -n "$pattern" ]]; then
|
|
111
418
|
# Normalize leading ./ so that ./src/ma matches pattern src/
|
|
112
|
-
local __norm_current="\${
|
|
419
|
+
local __norm_current="\${__file_current#./}"
|
|
113
420
|
local __norm_pattern="\${pattern#./}"
|
|
114
421
|
# For wildcard patterns, compare only the directory prefix before
|
|
115
422
|
# any wildcards so that typing src/ma narrows src/*.ts correctly
|
|
@@ -118,17 +425,34 @@ function _${programName} () {
|
|
|
118
425
|
__compare_pattern="\${__compare_pattern%%[\*\?]*}"
|
|
119
426
|
[[ "$__compare_pattern" == */* ]] && __compare_pattern="\${__compare_pattern%/*}/" || __compare_pattern=""
|
|
120
427
|
fi
|
|
121
|
-
|
|
428
|
+
local __extends_pattern=0
|
|
429
|
+
if [[ ( -n "$__compare_pattern" || -n "$__norm_current" ) && \${#__norm_current} -ge \${#__compare_pattern} && "\${__norm_current:0:\${#__compare_pattern}}" == "$__compare_pattern" && "$__file_current" != "$pattern" ]]; then
|
|
430
|
+
__extends_pattern=1
|
|
431
|
+
fi
|
|
432
|
+
if (( __extends_pattern )) && [[ "$__norm_pattern" != *[\*\?]* ]]; then
|
|
122
433
|
# User has typed beyond or an equivalent form of the pattern
|
|
123
434
|
true
|
|
124
435
|
else
|
|
436
|
+
# Wildcards keep their file filter while the typed prefix narrows
|
|
437
|
+
# the resulting paths. Save the expanded prefix before using pattern.
|
|
438
|
+
if [[ "$__norm_pattern" == *[\*\?]* ]]; then
|
|
439
|
+
__filter_pattern=1
|
|
440
|
+
__pattern_prefix="$__glob_current"
|
|
441
|
+
# An unquoted value equal to its wildcard is a complete search
|
|
442
|
+
# pattern, not a literal filename prefix to filter against.
|
|
443
|
+
if [ "$__file_current" = "$pattern" ] && [ "$__raw_file_current" = "$__file_current" ]; then
|
|
444
|
+
__pattern_prefix=""
|
|
445
|
+
fi
|
|
446
|
+
__pattern_tilde_prefix="$__tilde_prefix"
|
|
447
|
+
__pattern_tilde_expanded="$__tilde_expanded"
|
|
448
|
+
fi
|
|
125
449
|
__from_pattern=1
|
|
126
450
|
# Reset tilde state from the current-word expansion so that a
|
|
127
451
|
# non-tilde pattern is not rewritten through stale tilde state
|
|
128
452
|
__tilde_prefix=""
|
|
129
453
|
__tilde_expanded=""
|
|
130
454
|
__glob_current="$pattern"
|
|
131
|
-
if [[ "$pattern" =~
|
|
455
|
+
if [[ "$pattern" =~ $__tilde_glob_pattern ]]; then
|
|
132
456
|
__tilde_prefix="\${BASH_REMATCH[1]}"
|
|
133
457
|
eval "__tilde_expanded=\$__tilde_prefix" 2>/dev/null || true
|
|
134
458
|
if [[ -n "$__tilde_expanded" && "$__tilde_expanded" != "$__tilde_prefix" ]]; then
|
|
@@ -150,36 +474,93 @@ function _${programName} () {
|
|
|
150
474
|
# This runs after tilde expansion so that paths like ~/.config/ are
|
|
151
475
|
# checked against the expanded path, not the literal ~ string.
|
|
152
476
|
local __inside_hidden_path=0
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
477
|
+
local __hidden_directory_pattern='/[.]([^./][^/]*|[.][^/]+)/'
|
|
478
|
+
if [[ "/\${__glob_current%/}/" =~ $__hidden_directory_pattern ||
|
|
479
|
+
"/\${__pattern_prefix%/}/" =~ $__hidden_directory_pattern ]]; then
|
|
480
|
+
__inside_hidden_path=1
|
|
481
|
+
fi
|
|
156
482
|
# Also check if the current prefix explicitly targets hidden entries
|
|
157
483
|
# (e.g., user typed "." or ".e" to complete .env)
|
|
158
484
|
local __prefix_targets_hidden=0
|
|
159
485
|
local __prefix_base="\${__glob_current##*/}"
|
|
486
|
+
if [[ -n "$__pattern_prefix" ]]; then __prefix_base="\${__pattern_prefix##*/}"; fi
|
|
160
487
|
[[ "$__prefix_base" == .* ]] && __prefix_targets_hidden=1
|
|
161
488
|
if [[ "$hidden" == "1" || "$__inside_hidden_path" == "1" || "$__prefix_targets_hidden" == "1" ]]; then shopt -s dotglob; fi
|
|
162
489
|
|
|
163
490
|
# Pre-expand file candidates. When the glob base came from the
|
|
164
491
|
# program's pattern and contains wildcard characters (* or ?),
|
|
165
|
-
#
|
|
492
|
+
# expand it as data with shell globbing. Empty IFS avoids splitting paths;
|
|
493
|
+
# NUL records preserve spaces and newlines without requiring mapfile.
|
|
494
|
+
# Shell expansion respects dotglob on older Bash versions as well.
|
|
166
495
|
# Otherwise append * to treat it as a prefix.
|
|
167
|
-
#
|
|
168
|
-
#
|
|
496
|
+
# Only * and ? are wildcard syntax; escape brackets, backslashes, and
|
|
497
|
+
# extended-glob operators so other pattern characters stay literal.
|
|
169
498
|
local -a __candidates=()
|
|
170
499
|
if [[ "$__from_pattern" == "1" && "$__glob_current" == *[\*\?]* ]]; then
|
|
171
|
-
|
|
500
|
+
local __pathname_glob="" __glob_char __glob_index
|
|
501
|
+
for ((__glob_index=0; __glob_index<\${#__glob_current}; __glob_index++)); do
|
|
502
|
+
__glob_char="\${__glob_current:__glob_index:1}"
|
|
503
|
+
case "$__glob_char" in
|
|
504
|
+
'['|']'|'\\'|'('|')'|'|'|'+'|'@'|'!')
|
|
505
|
+
__pathname_glob+="\\\\$__glob_char" ;;
|
|
506
|
+
*) __pathname_glob+="$__glob_char" ;;
|
|
507
|
+
esac
|
|
508
|
+
done
|
|
509
|
+
[[ "$type" == "directory" ]] && __pathname_glob="\${__pathname_glob%/}"
|
|
510
|
+
local __glob_dir="\${__pathname_glob%/*}"
|
|
511
|
+
[[ "$__glob_dir" == "$__pathname_glob" ]] && __glob_dir="."
|
|
512
|
+
[[ -z "$__glob_dir" ]] && __glob_dir="/"
|
|
513
|
+
local __navigation_dir="" __basename_glob="\${__pathname_glob##*/}"
|
|
514
|
+
# Once an offered directory is entered, keep the basename filter but
|
|
515
|
+
# enumerate there. Only descendants of matching pattern bases qualify.
|
|
516
|
+
if [[ "$__pattern_prefix" == */* ]]; then
|
|
517
|
+
local __typed_dir="\${__pattern_prefix%/*}" __base_dir __norm_base __norm_dir
|
|
518
|
+
# Resolve directory ancestry in subshells, preserving the typed spelling
|
|
519
|
+
# for insertion. The trailing slash also preserves newline-ending names.
|
|
520
|
+
if __norm_dir=$(CDPATH= builtin cd -P -- "$__typed_dir" >/dev/null 2>&1 && printf '%s/' "\${PWD%/}"); then
|
|
521
|
+
while IFS= read -r -d '' __base_dir; do
|
|
522
|
+
[[ -d "$__base_dir" ]] || continue
|
|
523
|
+
__norm_base=$(CDPATH= builtin cd -P -- "$__base_dir" >/dev/null 2>&1 && printf '%s/' "\${PWD%/}") || continue
|
|
524
|
+
if [ "\${__norm_dir:0:\${#__norm_base}}" = "$__norm_base" ]; then
|
|
525
|
+
__navigation_dir="$__typed_dir"
|
|
526
|
+
break
|
|
527
|
+
fi
|
|
528
|
+
done < <(IFS=; printf '%s\\0' $__glob_dir)
|
|
529
|
+
fi
|
|
530
|
+
fi
|
|
531
|
+
local __candidate
|
|
532
|
+
while IFS= read -r -d '' __candidate; do
|
|
533
|
+
if [[ -e "$__candidate" || -L "$__candidate" ]]; then
|
|
534
|
+
[[ -d "$__candidate" ]] && __candidate="\${__candidate%/}"
|
|
535
|
+
__candidates+=("$__candidate")
|
|
536
|
+
fi
|
|
537
|
+
done < <(
|
|
538
|
+
IFS=
|
|
539
|
+
if [[ -n "$__navigation_dir" ]]; then
|
|
540
|
+
printf '%s\\0' "$__navigation_dir"/$__basename_glob
|
|
541
|
+
else
|
|
542
|
+
printf '%s\\0' $__pathname_glob
|
|
543
|
+
fi
|
|
544
|
+
)
|
|
172
545
|
# For file/any modes, also include directories from the base
|
|
173
546
|
# directory for navigation even when the glob itself does not
|
|
174
547
|
# match directory names. Skip this for directory mode so that
|
|
175
548
|
# the pattern's basename filter is respected.
|
|
176
549
|
if [[ "$type" != "directory" ]]; then
|
|
177
|
-
local __glob_dir="\${__glob_current%/*}"
|
|
178
|
-
[[ "$__glob_dir" == "$__glob_current" ]] && __glob_dir="."
|
|
179
550
|
local __d
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
551
|
+
local __directory_pattern="\${__glob_dir%/}/*/"
|
|
552
|
+
while IFS= read -r -d '' __d; do
|
|
553
|
+
[[ -d "$__d" ]] || continue
|
|
554
|
+
__d="\${__d%/}"
|
|
555
|
+
__candidates+=("$__d")
|
|
556
|
+
done < <(
|
|
557
|
+
IFS=
|
|
558
|
+
if [[ -n "$__navigation_dir" ]]; then
|
|
559
|
+
printf '%s\\0' "$__navigation_dir"/*/
|
|
560
|
+
else
|
|
561
|
+
printf '%s\\0' $__directory_pattern
|
|
562
|
+
fi
|
|
563
|
+
)
|
|
183
564
|
fi
|
|
184
565
|
else
|
|
185
566
|
__candidates=("$__glob_current"*)
|
|
@@ -190,6 +571,26 @@ function _${programName} () {
|
|
|
190
571
|
fi
|
|
191
572
|
fi
|
|
192
573
|
|
|
574
|
+
if (( __filter_pattern )); then
|
|
575
|
+
local __nocasematch_was_set=0
|
|
576
|
+
shopt -q nocasematch && __nocasematch_was_set=1
|
|
577
|
+
if shopt -q nocaseglob; then shopt -s nocasematch; else shopt -u nocasematch; fi
|
|
578
|
+
local -a __filtered_candidates=()
|
|
579
|
+
for __candidate in "\${__candidates[@]}"; do
|
|
580
|
+
if [[ "$__candidate" == "$__pattern_prefix"* ||
|
|
581
|
+
"\${__candidate#./}" == "\${__pattern_prefix#./}"* ]]; then
|
|
582
|
+
if [[ "$__pattern_prefix" == ./* && "$__candidate" != ./* ]]; then
|
|
583
|
+
__candidate="./$__candidate"
|
|
584
|
+
elif [[ "$__pattern_prefix" != ./* && "$__pattern_prefix" != . ]]; then
|
|
585
|
+
__candidate="\${__candidate#./}"
|
|
586
|
+
fi
|
|
587
|
+
__filtered_candidates+=("$__candidate")
|
|
588
|
+
fi
|
|
589
|
+
done
|
|
590
|
+
if (( __nocasematch_was_set )); then shopt -s nocasematch; else shopt -u nocasematch; fi
|
|
591
|
+
__candidates=("\${__filtered_candidates[@]}")
|
|
592
|
+
fi
|
|
593
|
+
|
|
193
594
|
# Generate file completions based on type
|
|
194
595
|
case "$type" in
|
|
195
596
|
file)
|
|
@@ -197,10 +598,11 @@ function _${programName} () {
|
|
|
197
598
|
if [[ -n "$extensions" ]]; then
|
|
198
599
|
# Files with extension filtering + directories
|
|
199
600
|
local ext_pattern="\${extensions//,/|}"
|
|
601
|
+
ext_pattern='\\.('"$ext_pattern"')$'
|
|
200
602
|
for item in "\${__candidates[@]}"; do
|
|
201
603
|
if [[ -d "$item" ]]; then
|
|
202
604
|
COMPREPLY+=("$item/")
|
|
203
|
-
elif [[ -f "$item" && "$item" =~
|
|
605
|
+
elif [[ -f "$item" && "$item" =~ $ext_pattern ]]; then
|
|
204
606
|
COMPREPLY+=("$item")
|
|
205
607
|
fi
|
|
206
608
|
done
|
|
@@ -227,10 +629,11 @@ function _${programName} () {
|
|
|
227
629
|
# Files with extension filtering + directories
|
|
228
630
|
# Files with extension filtering
|
|
229
631
|
local ext_pattern="\${extensions//,/|}"
|
|
632
|
+
ext_pattern='\\.('"$ext_pattern"')$'
|
|
230
633
|
for item in "\${__candidates[@]}"; do
|
|
231
634
|
if [[ -d "$item" ]]; then
|
|
232
635
|
COMPREPLY+=("$item/")
|
|
233
|
-
elif [[ ( -e "$item" || -L "$item" ) && "$item" =~
|
|
636
|
+
elif [[ ( -e "$item" || -L "$item" ) && "$item" =~ $ext_pattern ]]; then
|
|
234
637
|
COMPREPLY+=("$item")
|
|
235
638
|
fi
|
|
236
639
|
done
|
|
@@ -249,7 +652,12 @@ function _${programName} () {
|
|
|
249
652
|
esac
|
|
250
653
|
|
|
251
654
|
# Restore tilde prefix in completion results
|
|
252
|
-
if
|
|
655
|
+
if (( __filter_pattern )); then
|
|
656
|
+
__tilde_prefix="$__pattern_tilde_prefix"
|
|
657
|
+
__tilde_expanded="$__pattern_tilde_expanded"
|
|
658
|
+
fi
|
|
659
|
+
if [[ -n "$__tilde_prefix" &&
|
|
660
|
+
( -z "$__quote" || $__file_quote_offset -gt 0 ) ]]; then
|
|
253
661
|
local __i
|
|
254
662
|
for __i in "\${!COMPREPLY[@]}"; do
|
|
255
663
|
COMPREPLY[\$__i]="\${COMPREPLY[\$__i]/#\$__tilde_expanded/\$__tilde_prefix}"
|
|
@@ -264,36 +672,160 @@ function _${programName} () {
|
|
|
264
672
|
if [[ "$__failglob_was_set" == "1" ]]; then shopt -s failglob; fi
|
|
265
673
|
if [[ "$__noglob_was_set" == "1" ]]; then set -f; fi
|
|
266
674
|
|
|
675
|
+
# Readline appends directory separators when it receives the full path.
|
|
676
|
+
# Midword quotes still need ours because replies omit the typed prefix.
|
|
677
|
+
if [[ -n "$__quote" && $__file_quote_offset -eq 0 ]]; then
|
|
678
|
+
local __reply_index
|
|
679
|
+
for ((__reply_index=__reply_start; __reply_index<\${#COMPREPLY[@]}; __reply_index++)); do
|
|
680
|
+
COMPREPLY[__reply_index]="\${COMPREPLY[__reply_index]%/}"
|
|
681
|
+
done
|
|
682
|
+
fi
|
|
683
|
+
|
|
684
|
+
# Readline replaces only the text after an open quote. Remove the
|
|
685
|
+
# already typed prefix so it is not inserted a second time.
|
|
686
|
+
# Joined replies are trimmed once against __remove after restoring option=,
|
|
687
|
+
# and keyed replies once their key has been restored.
|
|
688
|
+
if [[ -n "$__quote" && $__file_quote_offset -gt 0 && -z "$__reply_prefix" && -z "$insertion_prefix" ]]; then
|
|
689
|
+
local __quote_prefix="\${__file_current:0:__file_quote_offset}"
|
|
690
|
+
local __reply_index
|
|
691
|
+
for ((__reply_index=__reply_start; __reply_index<\${#COMPREPLY[@]}; __reply_index++)); do
|
|
692
|
+
COMPREPLY[__reply_index]="\${COMPREPLY[__reply_index]#"$__quote_prefix"}"
|
|
693
|
+
done
|
|
694
|
+
fi
|
|
695
|
+
|
|
696
|
+
# Readline expands a leading tilde in a returned filename even when the
|
|
697
|
+
# user did not type one. Keep such local paths explicitly relative.
|
|
698
|
+
if [[ ( "$__raw_file_current" != "~"* || $__file_tilde_quoted == 1 ) && -z "$__tilde_prefix" ]]; then
|
|
699
|
+
local __reply_index
|
|
700
|
+
for ((__reply_index=__reply_start; __reply_index<\${#COMPREPLY[@]}; __reply_index++)); do
|
|
701
|
+
if [[ "\${COMPREPLY[__reply_index]}" =~ $__tilde_glob_pattern ]]; then
|
|
702
|
+
COMPREPLY[__reply_index]="./\${COMPREPLY[__reply_index]}"
|
|
703
|
+
fi
|
|
704
|
+
done
|
|
705
|
+
fi
|
|
706
|
+
|
|
707
|
+
# Bash 5.3 leaves substitutions unescaped inside an open double quote.
|
|
708
|
+
# Earlier versions already escape them during filename processing.
|
|
709
|
+
if [[ "$__quote" == '"' &&
|
|
710
|
+
( \${BASH_VERSINFO[0]} -gt 5 ||
|
|
711
|
+
( \${BASH_VERSINFO[0]} -eq 5 && \${BASH_VERSINFO[1]} -ge 3 ) ) &&
|
|
712
|
+
$(type -t compopt) == builtin ]]; then
|
|
713
|
+
local __escaped_reply __escape_index __reply_index
|
|
714
|
+
for ((__reply_index=__reply_start; __reply_index<\${#COMPREPLY[@]}; __reply_index++)); do
|
|
715
|
+
__escaped_reply=""
|
|
716
|
+
for ((__escape_index=0; __escape_index<\${#COMPREPLY[__reply_index]}; __escape_index++)); do
|
|
717
|
+
__char="\${COMPREPLY[__reply_index]:__escape_index:1}"
|
|
718
|
+
__next="\${COMPREPLY[__reply_index]:__escape_index+1:1}"
|
|
719
|
+
if [[ "$__char" == '\\' &&
|
|
720
|
+
( "$__next" == '$' || "$__next" == '\`' ) ]]; then
|
|
721
|
+
__escaped_reply+="\\\\$__char"
|
|
722
|
+
elif [[ "$__char" == '$' || "$__char" == '\`' ]]; then
|
|
723
|
+
__escaped_reply+="\\\\$__char"
|
|
724
|
+
else
|
|
725
|
+
__escaped_reply+="$__char"
|
|
726
|
+
fi
|
|
727
|
+
done
|
|
728
|
+
COMPREPLY[__reply_index]="$__escaped_reply"
|
|
729
|
+
done
|
|
730
|
+
fi
|
|
731
|
+
|
|
267
732
|
# Filter out hidden files unless requested
|
|
268
733
|
if [[ "$hidden" != "1" && "$__inside_hidden_path" == "0" && "$__prefix_targets_hidden" == "0" ]]; then
|
|
269
734
|
local filtered=()
|
|
270
735
|
local __name
|
|
271
736
|
for item in "\${COMPREPLY[@]}"; do
|
|
272
737
|
__name="\${item%/}"; __name="\${__name##*/}"
|
|
273
|
-
[[ "$__name" != .* ]]
|
|
738
|
+
if [[ "$__name" != .* ]]; then
|
|
739
|
+
filtered+=("$item")
|
|
740
|
+
fi
|
|
274
741
|
done
|
|
275
742
|
COMPREPLY=("\${filtered[@]}")
|
|
276
743
|
fi
|
|
744
|
+
if [[ -n "$insertion_prefix" ]]; then
|
|
745
|
+
local __reply_index
|
|
746
|
+
for ((__reply_index=__reply_start; __reply_index<\${#COMPREPLY[@]}; __reply_index++)); do
|
|
747
|
+
COMPREPLY[__reply_index]="$insertion_prefix\${COMPREPLY[__reply_index]}"
|
|
748
|
+
done
|
|
749
|
+
if [[ -n "$__quote" && $__file_quote_offset -gt 0 && -z "$__reply_prefix" ]]; then
|
|
750
|
+
local __quote_prefix="\${__key_current:0:__file_quote_offset}"
|
|
751
|
+
for ((__reply_index=__reply_start; __reply_index<\${#COMPREPLY[@]}; __reply_index++)); do
|
|
752
|
+
COMPREPLY[__reply_index]="\${COMPREPLY[__reply_index]#"$__quote_prefix"}"
|
|
753
|
+
done
|
|
754
|
+
fi
|
|
755
|
+
fi
|
|
756
|
+
if [[ \${#COMPREPLY[@]} -gt $__reply_start ]]; then __file_mode=1; fi
|
|
277
757
|
else
|
|
278
758
|
# Regular literal completion
|
|
279
759
|
COMPREPLY+=("$line")
|
|
280
760
|
fi
|
|
281
|
-
|
|
761
|
+
for __word in "\${COMPREPLY[@]}"; do
|
|
762
|
+
__replies+=("$__reply_prefix$__word")
|
|
763
|
+
done
|
|
764
|
+
done
|
|
765
|
+
COMPREPLY=()
|
|
766
|
+
for __word in "\${__replies[@]}"; do
|
|
767
|
+
if [[ "\${__word:0:\${#__remove}}" == "$__remove" ]]; then
|
|
768
|
+
COMPREPLY+=("\${__word:\${#__remove}}")
|
|
769
|
+
fi
|
|
770
|
+
done
|
|
771
|
+
if (( \${#COMPREPLY[@]} > 1 )) && command -v sort >/dev/null 2>&1; then
|
|
772
|
+
# Sort shell-escaped keys, not filenames: each key occupies one line even
|
|
773
|
+
# when a candidate contains tabs or newlines. Keep the first occurrence's
|
|
774
|
+
# index, then restore the original order without evaluating escaped data.
|
|
775
|
+
# This works without associative arrays or NUL-aware sort on Bash 3.2.
|
|
776
|
+
local -a __unique_replies=()
|
|
777
|
+
local __index __key __last_key=""
|
|
778
|
+
while IFS= read -r __index; do
|
|
779
|
+
__unique_replies+=("\${COMPREPLY[__index]}")
|
|
780
|
+
done < <(
|
|
781
|
+
for ((__index=0; __index<\${#COMPREPLY[@]}; __index++)); do
|
|
782
|
+
printf '%q\\t%d\\n' "\${COMPREPLY[__index]}" "$__index"
|
|
783
|
+
done |
|
|
784
|
+
LC_ALL=C sort -t $'\\t' -k1,1 -k2,2n |
|
|
785
|
+
{
|
|
786
|
+
while IFS=$'\\t' read -r __key __index; do
|
|
787
|
+
if [ "$__key" != "$__last_key" ]; then
|
|
788
|
+
printf '%s\\n' "$__index"
|
|
789
|
+
__last_key="$__key"
|
|
790
|
+
fi
|
|
791
|
+
done
|
|
792
|
+
} | LC_ALL=C sort -n
|
|
793
|
+
)
|
|
794
|
+
COMPREPLY=("\${__unique_replies[@]}")
|
|
795
|
+
fi
|
|
796
|
+
if [[ $__file_mode == 1 && $(type -t compopt) == builtin ]]; then
|
|
797
|
+
compopt -o filenames 2>/dev/null || true
|
|
798
|
+
fi
|
|
282
799
|
}
|
|
283
800
|
|
|
284
|
-
|
|
801
|
+
# Bash 3.2 has no compopt: use filename quoting for all candidates there.
|
|
802
|
+
if [[ $(type -t compopt) == builtin ]]; then
|
|
803
|
+
complete -F _${programName} -- ${programName}
|
|
804
|
+
else
|
|
805
|
+
complete -o filenames -F _${programName} -- ${programName}
|
|
806
|
+
fi
|
|
285
807
|
`;
|
|
286
808
|
},
|
|
287
809
|
*encodeSuggestions(suggestions) {
|
|
288
810
|
let i = 0;
|
|
289
811
|
for (const suggestion of suggestions) {
|
|
290
812
|
if (i > 0) yield "\n";
|
|
291
|
-
|
|
813
|
+
const joined = getJoinedFileSuggestion(suggestion);
|
|
814
|
+
if (joined !== void 0) {
|
|
815
|
+
if (joined.literal && suggestion.kind === "literal") {
|
|
816
|
+
yield encodeBashLiteral(suggestion.text);
|
|
817
|
+
yield "\n";
|
|
818
|
+
}
|
|
819
|
+
for (const [index, { prefix, file }] of joined.files.entries()) {
|
|
820
|
+
if (index > 0) yield "\n";
|
|
821
|
+
yield `__JOINED_FILE__:${encodePattern(prefix)}:${file.type}:${encodeExtensions(file.extensions)}:${encodePattern(sanitizeForTransport(file.pattern ?? ""))}:${file.includeHidden ? "1" : "0"}${encodeFilePrefix(file)}`;
|
|
822
|
+
}
|
|
823
|
+
} else if (suggestion.kind === "literal") yield encodeBashLiteral(suggestion.text);
|
|
292
824
|
else {
|
|
293
825
|
const extensions = encodeExtensions(suggestion.extensions);
|
|
294
826
|
const hidden = suggestion.includeHidden ? "1" : "0";
|
|
295
827
|
const pattern = encodePattern(suggestion.pattern ?? "");
|
|
296
|
-
yield `__FILE__:${suggestion.type}:${extensions}:${pattern}:${hidden}`;
|
|
828
|
+
yield `__FILE__:${suggestion.type}:${extensions}:${pattern}:${hidden}${encodeFilePrefix(suggestion)}`;
|
|
297
829
|
}
|
|
298
830
|
i++;
|
|
299
831
|
}
|
|
@@ -308,16 +840,36 @@ const zsh = {
|
|
|
308
840
|
generateScript(programName, args = []) {
|
|
309
841
|
validateProgramName(programName);
|
|
310
842
|
const escapedArgs = args.map((arg) => `'${arg.replace(/'/g, "'\\''")}'`).join(" ");
|
|
843
|
+
const functionName = /[.-]/.test(programName) ? `_optique:${programName}` : `_${programName}`;
|
|
844
|
+
const fixedGlobFunction = "__optique-fixed-glob-" + Array.from(programName, (character) => character.charCodeAt(0).toString(16).padStart(2, "0")).join("");
|
|
311
845
|
return `#compdef ${programName}
|
|
312
|
-
function
|
|
313
|
-
|
|
846
|
+
function ${functionName} () {
|
|
847
|
+
# Completion fields omit surrounding quotes, unlike words[CURRENT].
|
|
848
|
+
# Single-quoted fields are already literal; other fields retain escapes.
|
|
849
|
+
local current="\$IPREFIX\$PREFIX\$SUFFIX\$ISUFFIX"
|
|
850
|
+
local __logical_prefix="\$IPREFIX\$PREFIX"
|
|
851
|
+
# Let zsh match the suffix when completing in the middle of a word.
|
|
852
|
+
if [[ -o complete_in_word ]]; then
|
|
853
|
+
current="\$__logical_prefix"
|
|
854
|
+
fi
|
|
855
|
+
if [[ "\$compstate[quote]" == '\"' ]]; then
|
|
856
|
+
# Restore the omitted quoting context before removing shell escapes.
|
|
857
|
+
# In double quotes, a backslash before an ordinary character is literal.
|
|
858
|
+
current="\$QIPREFIX\$current\${QISUFFIX:-\$compstate[quote]}"
|
|
859
|
+
__logical_prefix="\$QIPREFIX\$__logical_prefix\$compstate[quote]"
|
|
860
|
+
current="\${(Q)current}"
|
|
861
|
+
__logical_prefix="\${(Q)__logical_prefix}"
|
|
862
|
+
elif [[ "\$compstate[quote]" != "'" ]]; then
|
|
863
|
+
current="\${(Q)current}"
|
|
864
|
+
__logical_prefix="\${(Q)__logical_prefix}"
|
|
865
|
+
fi
|
|
314
866
|
local -a prev
|
|
315
867
|
# Extract previous arguments, skipping empty ones
|
|
316
868
|
prev=()
|
|
317
869
|
local i
|
|
318
870
|
for (( i=2; i < CURRENT; i++ )); do
|
|
319
871
|
if [[ -n "\$words[i]" ]]; then
|
|
320
|
-
prev+=("\$words[i]")
|
|
872
|
+
prev+=("\${(Q)words[i]}")
|
|
321
873
|
fi
|
|
322
874
|
done
|
|
323
875
|
|
|
@@ -341,11 +893,34 @@ function _${programName.replace(/[^a-zA-Z0-9]/g, "_")} () {
|
|
|
341
893
|
desc=\${line#*\$'\\0'}
|
|
342
894
|
desc=\${desc%%\$'\\0'*}
|
|
343
895
|
|
|
344
|
-
|
|
896
|
+
local __literal=0
|
|
897
|
+
if [[ "\$value" == __LITERAL__:* ]]; then
|
|
898
|
+
value="\${value#__LITERAL__:}"
|
|
899
|
+
__literal=1
|
|
900
|
+
fi
|
|
901
|
+
if [[ \$__literal == 0 && "\$value" == __FILE__:* ]]; then
|
|
345
902
|
# 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
|
-
|
|
903
|
+
local type extensions pattern hidden insertion_prefix
|
|
904
|
+
IFS=':' read -r _ type extensions pattern hidden insertion_prefix <<< "\$value"
|
|
905
|
+
local __decoded_pattern="\$pattern"
|
|
906
|
+
__decoded_pattern="\${__decoded_pattern//\\%3A/:}"; __decoded_pattern="\${__decoded_pattern//\\%25/%}"
|
|
907
|
+
insertion_prefix="\${insertion_prefix//\\%3A/:}"; insertion_prefix="\${insertion_prefix//\\%25/%}"
|
|
908
|
+
local __saved_prefix="\$PREFIX" __saved_iprefix="\$IPREFIX"
|
|
909
|
+
local __saved_current="\$current" __saved_logical_prefix="\$__logical_prefix"
|
|
910
|
+
if [[ -n "\$insertion_prefix" && "\$__decoded_pattern" == "\$insertion_prefix"* &&
|
|
911
|
+
"\$PREFIX" == "\$insertion_prefix"* &&
|
|
912
|
+
"\$__logical_prefix" == "\$insertion_prefix"* &&
|
|
913
|
+
"\$current" == "\$insertion_prefix"* ]]; then
|
|
914
|
+
pattern="\${__decoded_pattern#"\$insertion_prefix"}"
|
|
915
|
+
PREFIX="\${PREFIX#"\$insertion_prefix"}"
|
|
916
|
+
IPREFIX="\$IPREFIX\$insertion_prefix"
|
|
917
|
+
# Compare file patterns with the value part, not the inserted key.
|
|
918
|
+
current="\${current#"\$insertion_prefix"}"
|
|
919
|
+
__logical_prefix="\${__logical_prefix#"\$insertion_prefix"}"
|
|
920
|
+
else
|
|
921
|
+
# Preserve legacy decoding for directives without active metadata.
|
|
922
|
+
pattern="\${pattern//%3A/:}"; pattern="\${pattern//%25/%}"
|
|
923
|
+
fi
|
|
349
924
|
has_file_completion=1
|
|
350
925
|
|
|
351
926
|
# Native completion helpers expect normal zsh glob grouping even if
|
|
@@ -353,6 +928,11 @@ function _${programName.replace(/[^a-zA-Z0-9]/g, "_")} () {
|
|
|
353
928
|
local __was_sh_glob=0
|
|
354
929
|
[[ -o sh_glob ]] && __was_sh_glob=1
|
|
355
930
|
unsetopt sh_glob
|
|
931
|
+
local __was_extended_glob=0
|
|
932
|
+
[[ -o extended_glob ]] && __was_extended_glob=1
|
|
933
|
+
local __was_sh_word_split=0
|
|
934
|
+
[[ -o sh_word_split ]] && __was_sh_word_split=1
|
|
935
|
+
unsetopt sh_word_split
|
|
356
936
|
|
|
357
937
|
# Enable glob_dots when hidden files are requested so that
|
|
358
938
|
# _files and _directories include dot-prefixed entries
|
|
@@ -364,21 +944,351 @@ function _${programName.replace(/[^a-zA-Z0-9]/g, "_")} () {
|
|
|
364
944
|
# _directories enumerate the pattern's directory instead of the
|
|
365
945
|
# current word. If the user has already typed beyond the pattern,
|
|
366
946
|
# keep PREFIX unchanged for incremental narrowing.
|
|
367
|
-
local
|
|
368
|
-
|
|
947
|
+
local __fixed_glob=0
|
|
948
|
+
local -a __fixed_matches
|
|
949
|
+
local __initial_file_pattern="" __initial_directory="" __initial_leaf=""
|
|
950
|
+
if [[ ( "\$type" == "file" || "\$type" == "any" || "\$type" == "directory" ) &&
|
|
951
|
+
-z "\$current\$PREFIX\$SUFFIX" &&
|
|
952
|
+
-z "\${compstate[pattern_match]}" &&
|
|
953
|
+
"\$pattern" != /* && "\$pattern" != "~"* ]]; then
|
|
954
|
+
if [[ "\$pattern" == */* ]]; then
|
|
955
|
+
__initial_directory="\${pattern%/*}/"
|
|
956
|
+
fi
|
|
957
|
+
__initial_leaf="\${pattern##*/}"
|
|
958
|
+
# Native -g filters are split as shell words and use extended glob
|
|
959
|
+
# syntax. Keep unsupported filename forms on the existing path.
|
|
960
|
+
if [[ "\$__initial_leaf" == *[\*\?]* &&
|
|
961
|
+
"\$__initial_directory" != *[\*\?]* &&
|
|
962
|
+
"\$__initial_directory" != *["[]()"]* &&
|
|
963
|
+
"\$__initial_leaf" != *["[]"]* &&
|
|
964
|
+
"\$__initial_leaf" != *'\\'* &&
|
|
965
|
+
"\$__initial_leaf" != *[[:space:]]* &&
|
|
966
|
+
"\$__initial_leaf" != *["#~^()|<>;&"]* &&
|
|
967
|
+
"\$__initial_leaf" != *"'"* && "\$__initial_leaf" != *'"'* ]]; then
|
|
968
|
+
__initial_file_pattern="\$__initial_leaf"
|
|
969
|
+
# Only the literal directory belongs in PREFIX. Quote it so
|
|
970
|
+
# spaces and shell metacharacters remain literal path text.
|
|
971
|
+
if [[ -n "\$__initial_directory" ]]; then
|
|
972
|
+
PREFIX="\${(q)__initial_directory}"
|
|
973
|
+
fi
|
|
974
|
+
fi
|
|
975
|
+
fi
|
|
976
|
+
local __saved_pattern_match="\$compstate[pattern_match]"
|
|
977
|
+
local __pattern_match_changed=0
|
|
978
|
+
# path() echoes current as its pattern. Keep zsh's escaped PREFIX
|
|
979
|
+
# in that case instead of reinterpreting logical text as shell syntax.
|
|
980
|
+
if [[ -n "\$pattern" && "\$pattern" != "\$current" && -z "\$__initial_file_pattern" ]]; then
|
|
369
981
|
# Normalize leading ./ so that ./src/ma matches pattern src/
|
|
370
|
-
local __norm_prefix="\${
|
|
982
|
+
local __norm_prefix="\${__logical_prefix#./}"
|
|
371
983
|
local __norm_pattern="\${pattern#./}"
|
|
372
984
|
local __compare_pattern="\$__norm_pattern"
|
|
373
985
|
if [[ "\$__compare_pattern" == *[\*\?]* ]]; then
|
|
374
986
|
__compare_pattern="\${__compare_pattern%%[\*\?]*}"
|
|
375
987
|
[[ "\$__compare_pattern" == */* ]] && __compare_pattern="\${__compare_pattern%/*}/" || __compare_pattern=""
|
|
376
988
|
fi
|
|
377
|
-
if [[ ( -n "\$__compare_pattern" || -n "\$__norm_prefix" ) && \${#__norm_prefix} -ge \${#__compare_pattern} && "\${__norm_prefix[1,\${#__compare_pattern}]}" == "\$__compare_pattern" && "\$
|
|
378
|
-
#
|
|
379
|
-
|
|
989
|
+
if [[ ( -n "\$__compare_pattern" || -n "\$__norm_prefix" ) && \${#__norm_prefix} -ge \${#__compare_pattern} && "\${__norm_prefix[1,\${#__compare_pattern}]}" == "\$__compare_pattern" && "\$__logical_prefix" != "\$pattern" ]]; then
|
|
990
|
+
# Keep the typed prefix, but retain a fixed file glob separately.
|
|
991
|
+
if [[ "\$type" != "directory" && "\$__norm_pattern" == *[\*\?]* ]]; then
|
|
992
|
+
__fixed_glob=1
|
|
993
|
+
local __fixed_was_glob_dots=0
|
|
994
|
+
[[ -o glob_dots ]] && __fixed_was_glob_dots=1
|
|
995
|
+
if [[ "\$PREFIX" == .* || "\$PREFIX" == */.* ]]; then setopt glob_dots; fi
|
|
996
|
+
# Keep qualifiers in a string so sourcing also works with SH_GLOB.
|
|
997
|
+
local __fixed_pattern="" __glob_char="" __glob_previous=""
|
|
998
|
+
local __glob_escaped=0
|
|
999
|
+
local __glob_class=0 __glob_class_start=0 __glob_class_nested=""
|
|
1000
|
+
local -i __glob_index
|
|
1001
|
+
for (( __glob_index=1; __glob_index <= \${#pattern}; __glob_index++ )); do
|
|
1002
|
+
__glob_char="\${pattern[__glob_index]}"
|
|
1003
|
+
if [[ "\$__glob_class" == "1" ]]; then
|
|
1004
|
+
# Class members are already literal. Preserve leading ]
|
|
1005
|
+
# and nested POSIX classes without adding backslashes.
|
|
1006
|
+
__fixed_pattern+="\$__glob_char"
|
|
1007
|
+
if [[ -n "\$__glob_class_nested" ]]; then
|
|
1008
|
+
if [[ "\$__glob_char" == "]" && "\$__glob_previous" == "\$__glob_class_nested" ]]; then
|
|
1009
|
+
__glob_class_nested=""
|
|
1010
|
+
fi
|
|
1011
|
+
elif [[ "\$__glob_char" == "[" && "\${pattern[__glob_index+1]}" == ":" ]]; then
|
|
1012
|
+
__glob_class_nested="\${pattern[__glob_index+1]}"
|
|
1013
|
+
__glob_class_start=0
|
|
1014
|
+
elif [[ "\$__glob_char" == "]" && "\$__glob_class_start" == "0" ]]; then
|
|
1015
|
+
__glob_class=0
|
|
1016
|
+
elif [[ "\$__glob_class_start" == "1" && ( "\$__glob_char" == "!" || "\$__glob_char" == "^" ) &&
|
|
1017
|
+
( "\${pattern[__glob_index+1]}" != "]" || "\${pattern[__glob_index+2,-1]}" == *']'* ) ]]; then
|
|
1018
|
+
__glob_class_start=2
|
|
1019
|
+
else
|
|
1020
|
+
__glob_class_start=0
|
|
1021
|
+
fi
|
|
1022
|
+
__glob_previous="\$__glob_char"
|
|
1023
|
+
continue
|
|
1024
|
+
fi
|
|
1025
|
+
if [[ "\$__glob_escaped" == "1" ]]; then
|
|
1026
|
+
__fixed_pattern+="\$__glob_char"
|
|
1027
|
+
__glob_escaped=0
|
|
1028
|
+
__glob_previous=""
|
|
1029
|
+
continue
|
|
1030
|
+
fi
|
|
1031
|
+
if [[ "\$__glob_char" == "\\\\" ]]; then
|
|
1032
|
+
__fixed_pattern+="\$__glob_char"
|
|
1033
|
+
__glob_escaped=1
|
|
1034
|
+
elif [[ "\$__glob_char" == "[" ]]; then
|
|
1035
|
+
__fixed_pattern+="\$__glob_char"
|
|
1036
|
+
__glob_class=1
|
|
1037
|
+
__glob_class_start=1
|
|
1038
|
+
elif [[ "\$__glob_char" == [#^~\\(\\)\\|\\<\\>] &&
|
|
1039
|
+
! ( "\$__glob_char" == "~" && "\$__glob_index" == "1" ) ]]; then
|
|
1040
|
+
# Keep class negation and leading home expansion; escape
|
|
1041
|
+
# extended-glob operators in literal pathname components.
|
|
1042
|
+
__fixed_pattern+="\\\\\$__glob_char"
|
|
1043
|
+
else
|
|
1044
|
+
__fixed_pattern+="\$__glob_char"
|
|
1045
|
+
fi
|
|
1046
|
+
__glob_previous="\$__glob_char"
|
|
1047
|
+
done
|
|
1048
|
+
__fixed_pattern+="(#qN)"
|
|
1049
|
+
__fixed_matches=( \${~__fixed_pattern} )
|
|
1050
|
+
__fixed_matches=( "\${(@)__fixed_matches:a}" )
|
|
1051
|
+
# Hidden parents widen fixed matches, not native child suggestions.
|
|
1052
|
+
if [[ "\$__fixed_was_glob_dots" == "0" && "\${PREFIX##*/}" != .* ]]; then unsetopt glob_dots; fi
|
|
1053
|
+
fi
|
|
380
1054
|
else
|
|
381
|
-
|
|
1055
|
+
local __native_pattern="\$pattern"
|
|
1056
|
+
# Initial globs use the conservative native filters above.
|
|
1057
|
+
# Retained input and protected markers still need pattern matching.
|
|
1058
|
+
if [[ -n "\$compstate[context]" && "\$pattern" == *[\\*\\?\\[]* &&
|
|
1059
|
+
( -n "\$current" || "\$pattern" == *'$'* || "\$pattern" == *'\`'* ) ]]; then
|
|
1060
|
+
compstate[pattern_match]='*'
|
|
1061
|
+
__pattern_match_changed=1
|
|
1062
|
+
fi
|
|
1063
|
+
# Native helpers expand substitutions, tilde names and executable
|
|
1064
|
+
# glob qualifiers. Input-derived patterns must keep these literal.
|
|
1065
|
+
# Ordinary fixed glob/tilde patterns retain their native behavior.
|
|
1066
|
+
local __tilde_name="\${pattern#\\~}"
|
|
1067
|
+
__tilde_name="\${__tilde_name%%/*}"
|
|
1068
|
+
local -a match mbegin mend
|
|
1069
|
+
local __code_qualifier=0
|
|
1070
|
+
local __fixed_qualifier_base=""
|
|
1071
|
+
local __safe_qualifiers='([-/F.@=p*rwxAIERWXsStUG^MTNDn,]|[ugdl]<->|[amc]([Mwhmsd]|)([-+]|)<->|L([kKmMgGtTpP]|)([-+]|)<->|[oO][nLlamcdN])##'
|
|
1072
|
+
local __qualifier_pattern="\$pattern" __qualifier
|
|
1073
|
+
while _have_glob_qual "\$__qualifier_pattern" complete; do
|
|
1074
|
+
__qualifier="\${match[5]#\\#q}"
|
|
1075
|
+
__qualifier_pattern="\$match[2]"
|
|
1076
|
+
# A numeric sense (+1) is not a function qualifier (+name).
|
|
1077
|
+
# Accept only bounded attribute/sort syntax in this exception.
|
|
1078
|
+
# Expand the pattern at runtime after unsetting SH_GLOB.
|
|
1079
|
+
if [[ "\$__qualifier" == \${~__safe_qualifiers} ]]; then
|
|
1080
|
+
__fixed_qualifier_base="\$__qualifier_pattern"
|
|
1081
|
+
elif [[ "\$__qualifier" == *[e+]* ]]; then
|
|
1082
|
+
__code_qualifier=1
|
|
1083
|
+
fi
|
|
1084
|
+
done
|
|
1085
|
+
# Partial overlaps with complete backend classes/groups remain
|
|
1086
|
+
# syntax. A complete input-provided expression can be literal.
|
|
1087
|
+
local -a __syntax_starts=() __syntax_ends=() __group_stack=()
|
|
1088
|
+
local -i __scan_index __class_begin=0 __member_begin=0
|
|
1089
|
+
local __scan_char __nested_class=""
|
|
1090
|
+
for (( __scan_index=1; __scan_index <= \${#pattern}; __scan_index++ )); do
|
|
1091
|
+
__scan_char="\${pattern[__scan_index]}"
|
|
1092
|
+
if [[ "\$__scan_char" == '\\' ]]; then
|
|
1093
|
+
(( __scan_index++ ))
|
|
1094
|
+
elif (( __class_begin )); then
|
|
1095
|
+
if [[ "\$__scan_char" == '[' && "\${pattern[__scan_index+1]}" == [:.=] ]]; then
|
|
1096
|
+
__nested_class="\${pattern[__scan_index+1]}"
|
|
1097
|
+
elif [[ -n "\$__nested_class" && "\$__scan_char" == "\$__nested_class" && "\${pattern[__scan_index+1]}" == ']' ]]; then
|
|
1098
|
+
(( __scan_index++ ))
|
|
1099
|
+
__nested_class=""
|
|
1100
|
+
elif [[ -z "\$__nested_class" && "\$__scan_char" == ']' ]] && (( __scan_index > __member_begin )); then
|
|
1101
|
+
__syntax_starts+=("\$__class_begin")
|
|
1102
|
+
__syntax_ends+=("\$__scan_index")
|
|
1103
|
+
__class_begin=0
|
|
1104
|
+
elif (( __scan_index == __member_begin )) &&
|
|
1105
|
+
[[ "\$__scan_char" == '!' || "\$__scan_char" == '^' ]] &&
|
|
1106
|
+
[[ "\${pattern[__scan_index+1]}" != ']' || "\${pattern[__scan_index+2,-1]}" == *']'* ]]; then
|
|
1107
|
+
(( __member_begin++ ))
|
|
1108
|
+
fi
|
|
1109
|
+
elif [[ "\$__scan_char" == '[' ]]; then
|
|
1110
|
+
__class_begin=\$__scan_index
|
|
1111
|
+
__member_begin=\$(( __scan_index + 1 ))
|
|
1112
|
+
elif [[ "\$__scan_char" == '(' ]]; then
|
|
1113
|
+
__group_stack+=("\$__scan_index")
|
|
1114
|
+
elif [[ "\$__scan_char" == ')' ]] && (( \${#__group_stack} )); then
|
|
1115
|
+
__syntax_starts+=("\${__group_stack[-1]}")
|
|
1116
|
+
__syntax_ends+=("\$__scan_index")
|
|
1117
|
+
__group_stack[-1]=()
|
|
1118
|
+
fi
|
|
1119
|
+
done
|
|
1120
|
+
# Search retained input only where it cannot split fixed syntax.
|
|
1121
|
+
local __input_part="\$current"
|
|
1122
|
+
local __backend_prefix="\$pattern" __backend_suffix=""
|
|
1123
|
+
local __search_pattern __candidate_prefix
|
|
1124
|
+
local -i __search_offset __overlap_start __overlap_end __range_index __syntax_overlap
|
|
1125
|
+
local -i __fixed_tilde_end=0
|
|
1126
|
+
local __tilde_expression='([+-]|[+-]<->|[[:alnum:]_.-]#)'
|
|
1127
|
+
if [[ "\$pattern" == '~'* &&
|
|
1128
|
+
"\$__tilde_name" == \${~__tilde_expression} ]]; then
|
|
1129
|
+
__fixed_tilde_end=\$(( \${#__tilde_name} + 1 ))
|
|
1130
|
+
fi
|
|
1131
|
+
while [[ -n "\$__input_part" ]]; do
|
|
1132
|
+
__search_pattern="\$pattern"
|
|
1133
|
+
__search_offset=0
|
|
1134
|
+
while [[ "\$__search_pattern" == *"\$__input_part"* ]]; do
|
|
1135
|
+
__candidate_prefix="\${__search_pattern%%"\$__input_part"*}"
|
|
1136
|
+
__overlap_start=\$(( __search_offset + \${#__candidate_prefix} + 1 ))
|
|
1137
|
+
__overlap_end=\$(( __overlap_start + \${#__input_part} - 1 ))
|
|
1138
|
+
__syntax_overlap=0
|
|
1139
|
+
# Leading home/directory-stack/named-directory syntax belongs
|
|
1140
|
+
# to the fixed pattern, even when the input overlaps it.
|
|
1141
|
+
if (( __fixed_tilde_end && __overlap_start <= __fixed_tilde_end )); then
|
|
1142
|
+
__syntax_overlap=1
|
|
1143
|
+
fi
|
|
1144
|
+
for (( __range_index=1; __range_index <= \${#__syntax_starts}; __range_index++ )); do
|
|
1145
|
+
if (( __overlap_start <= __syntax_ends[__range_index] &&
|
|
1146
|
+
__overlap_end >= __syntax_starts[__range_index] &&
|
|
1147
|
+
( __overlap_start > __syntax_starts[__range_index] ||
|
|
1148
|
+
__overlap_end < __syntax_ends[__range_index] ) )); then
|
|
1149
|
+
__syntax_overlap=1
|
|
1150
|
+
break
|
|
1151
|
+
fi
|
|
1152
|
+
done
|
|
1153
|
+
# Every position in a recognized fixed attribute qualifier
|
|
1154
|
+
# is backend syntax, including its closing parenthesis.
|
|
1155
|
+
if [[ "\$__fixed_qualifier_base" == *[\\*\\?\\]] ]] &&
|
|
1156
|
+
(( __overlap_end > \${#__fixed_qualifier_base} )); then
|
|
1157
|
+
__syntax_overlap=1
|
|
1158
|
+
fi
|
|
1159
|
+
if (( ! __syntax_overlap )); then
|
|
1160
|
+
if (( __overlap_start == 1 )); then
|
|
1161
|
+
__backend_prefix=""
|
|
1162
|
+
else
|
|
1163
|
+
__backend_prefix="\${pattern[1,__overlap_start-1]}"
|
|
1164
|
+
fi
|
|
1165
|
+
__backend_suffix="\${pattern[__overlap_end+1,-1]}"
|
|
1166
|
+
break 2
|
|
1167
|
+
fi
|
|
1168
|
+
__search_offset=\$__overlap_start
|
|
1169
|
+
__search_pattern="\${pattern[__overlap_start+1,-1]}"
|
|
1170
|
+
done
|
|
1171
|
+
__input_part="\${__input_part[2,-1]}"
|
|
1172
|
+
done
|
|
1173
|
+
local __quoted_input="\${(q)__input_part}"
|
|
1174
|
+
# Pattern matching removes quoting differently from normal
|
|
1175
|
+
# completion. Character classes keep these characters literal.
|
|
1176
|
+
if [[ -n "\$compstate[pattern_match]" ]]; then
|
|
1177
|
+
__quoted_input="\${(b)__input_part}"
|
|
1178
|
+
__quoted_input="\${__quoted_input//\\\$/[\\\$]}"
|
|
1179
|
+
__quoted_input="\${__quoted_input//\\\`/[\\\`]}"
|
|
1180
|
+
fi
|
|
1181
|
+
if [[ "\$pattern" == *'$'* || "\$pattern" == *'\`'* ||
|
|
1182
|
+
( "\$pattern" == '~'* && "\$__tilde_name" == *[^[:alnum:]_.+-]* ) ]] ||
|
|
1183
|
+
(( __code_qualifier )); then
|
|
1184
|
+
__native_pattern="\${(q)pattern}"
|
|
1185
|
+
local __backend_pattern="\$__backend_prefix\$__backend_suffix"
|
|
1186
|
+
local __backend_tilde="\${__backend_pattern#\\~}"
|
|
1187
|
+
__backend_tilde="\${__backend_tilde%%/*}"
|
|
1188
|
+
local __unsafe_backend_qualifier=0
|
|
1189
|
+
__qualifier_pattern="\$__backend_pattern"
|
|
1190
|
+
while _have_glob_qual "\$__qualifier_pattern" complete; do
|
|
1191
|
+
__qualifier="\${match[5]#\\#q}"
|
|
1192
|
+
__qualifier_pattern="\$match[2]"
|
|
1193
|
+
if [[ "\$__qualifier" != \${~__safe_qualifiers} ]]; then
|
|
1194
|
+
__unsafe_backend_qualifier=1
|
|
1195
|
+
fi
|
|
1196
|
+
done
|
|
1197
|
+
# Retain trusted wildcards and safe attribute qualifiers when
|
|
1198
|
+
# all executable syntax belongs to the quoted input.
|
|
1199
|
+
if [[ -n "\$__input_part" &&
|
|
1200
|
+
"\$__backend_pattern" != *'$'* &&
|
|
1201
|
+
"\$__backend_pattern" != *'\`'* &&
|
|
1202
|
+
( "\$__backend_pattern" != '~'* ||
|
|
1203
|
+
"\$__backend_tilde" != *[^[:alnum:]_.+-]* ) ]] &&
|
|
1204
|
+
(( ! __unsafe_backend_qualifier )); then
|
|
1205
|
+
__native_pattern="\$__backend_prefix\$__quoted_input\$__backend_suffix"
|
|
1206
|
+
elif [[ "\$__backend_pattern" != *'$('* &&
|
|
1207
|
+
"\$__backend_pattern" != *'\${'* &&
|
|
1208
|
+
"\$__backend_pattern" != *'$['* &&
|
|
1209
|
+
( "\$pattern" != '~'* ||
|
|
1210
|
+
"\$__tilde_name" != *[^[:alnum:]_.+-]* ) ]] &&
|
|
1211
|
+
(( ! __unsafe_backend_qualifier )); then
|
|
1212
|
+
# Fixed patterns also need literal substitution markers,
|
|
1213
|
+
# without quoting their wildcards or safe qualifiers.
|
|
1214
|
+
local __dollar='\\\$'
|
|
1215
|
+
local __backtick='\\\`'
|
|
1216
|
+
if [[ -n "\$compstate[pattern_match]" ]]; then
|
|
1217
|
+
__dollar='[\$]'
|
|
1218
|
+
__backtick='[\`]'
|
|
1219
|
+
fi
|
|
1220
|
+
# Existing glob classes already protect their markers.
|
|
1221
|
+
local -a __backend_parts=()
|
|
1222
|
+
local __part __escaped __char __class_kind
|
|
1223
|
+
local -i __index __in_class __class_start
|
|
1224
|
+
for __part in "\$__backend_prefix" "\$__backend_suffix"; do
|
|
1225
|
+
__escaped=""
|
|
1226
|
+
__in_class=0
|
|
1227
|
+
__class_kind=""
|
|
1228
|
+
for (( __index=1; __index <= \${#__part}; __index++ )); do
|
|
1229
|
+
__char="\${__part[__index]}"
|
|
1230
|
+
if [[ "\$__char" == '\\' ]]; then
|
|
1231
|
+
__escaped+="\$__char\${__part[__index+1]}"
|
|
1232
|
+
(( __index++ ))
|
|
1233
|
+
continue
|
|
1234
|
+
elif (( __in_class )); then
|
|
1235
|
+
if [[ "\$__char" == '[' && "\${__part[__index+1]}" == [:.=] ]]; then
|
|
1236
|
+
__class_kind="\${__part[__index+1]}"
|
|
1237
|
+
elif [[ -n "\$__class_kind" && "\$__char" == "\$__class_kind" && "\${__part[__index+1]}" == ']' ]]; then
|
|
1238
|
+
__escaped+="\$__char]"
|
|
1239
|
+
(( __index++ ))
|
|
1240
|
+
__class_kind=""
|
|
1241
|
+
continue
|
|
1242
|
+
elif [[ -z "\$__class_kind" && "\$__char" == ']' ]] && (( __index > __class_start )); then
|
|
1243
|
+
__in_class=0
|
|
1244
|
+
elif (( __index == __class_start )) && [[ "\$__char" == '!' || "\$__char" == '^' ]]; then
|
|
1245
|
+
(( __class_start++ ))
|
|
1246
|
+
fi
|
|
1247
|
+
elif [[ "\$__char" == '[' ]]; then
|
|
1248
|
+
__in_class=1
|
|
1249
|
+
__class_start=$(( __index + 1 ))
|
|
1250
|
+
elif [[ "\$__char" == '$' ]]; then
|
|
1251
|
+
__char="\$__dollar"
|
|
1252
|
+
elif [[ "\$__char" == '\`' ]]; then
|
|
1253
|
+
__char="\$__backtick"
|
|
1254
|
+
fi
|
|
1255
|
+
__escaped+="\$__char"
|
|
1256
|
+
done
|
|
1257
|
+
__backend_parts+=("\$__escaped")
|
|
1258
|
+
done
|
|
1259
|
+
__backend_prefix="\${__backend_parts[1]}"
|
|
1260
|
+
__backend_suffix="\${__backend_parts[2]}"
|
|
1261
|
+
__native_pattern="\$__backend_prefix\$__quoted_input\$__backend_suffix"
|
|
1262
|
+
fi
|
|
1263
|
+
else
|
|
1264
|
+
# Quote retained metacharacters, including segments made only
|
|
1265
|
+
# of them. Wildcard-only overlap remains backend syntax.
|
|
1266
|
+
if [[ "\$__input_part" == *[\\\\\\(\\)\\[\\#\\^\\|\\~\\<\\>\\*\\?]* &&
|
|
1267
|
+
"\$__input_part" != [\\*\\?]## ]]; then
|
|
1268
|
+
__native_pattern="\$__backend_prefix\$__quoted_input\$__backend_suffix"
|
|
1269
|
+
fi
|
|
1270
|
+
fi
|
|
1271
|
+
PREFIX="\$__native_pattern"
|
|
1272
|
+
# Match simple fixed tilde globs through native -g filters. The
|
|
1273
|
+
# pattern matcher otherwise treats the leading ~ as glob syntax,
|
|
1274
|
+
# even after _path_files has expanded it for filesystem lookup.
|
|
1275
|
+
if (( __fixed_tilde_end )) && [[ -z "\$__input_part" &&
|
|
1276
|
+
"\$__native_pattern" == "\$pattern" && "\$pattern" == */* ]]; then
|
|
1277
|
+
__initial_directory="\${pattern%/*}/"
|
|
1278
|
+
__initial_leaf="\${pattern##*/}"
|
|
1279
|
+
if [[ "\$__initial_leaf" == *[\*\?]* &&
|
|
1280
|
+
"\$__initial_directory" != *[\*\?]* &&
|
|
1281
|
+
"\$__initial_directory" != *["[]()"]* &&
|
|
1282
|
+
"\$__initial_leaf" != *["[]()#~^|<>;&"]* &&
|
|
1283
|
+
"\$__initial_leaf" != *'\\'* &&
|
|
1284
|
+
"\$__initial_leaf" != *[[:space:]]* &&
|
|
1285
|
+
"\$__initial_leaf" != *"'"* && "\$__initial_leaf" != *'"'* ]]; then
|
|
1286
|
+
__initial_file_pattern="\$__initial_leaf"
|
|
1287
|
+
PREFIX="\$__initial_directory"
|
|
1288
|
+
compstate[pattern_match]=""
|
|
1289
|
+
__pattern_match_changed=1
|
|
1290
|
+
fi
|
|
1291
|
+
fi
|
|
382
1292
|
fi
|
|
383
1293
|
fi
|
|
384
1294
|
|
|
@@ -390,15 +1300,114 @@ function _${programName.replace(/[^a-zA-Z0-9]/g, "_")} () {
|
|
|
390
1300
|
ext_pattern="*.\$extensions"
|
|
391
1301
|
fi
|
|
392
1302
|
fi
|
|
1303
|
+
# _wanted/_next_label write to the named explanation parameter.
|
|
1304
|
+
# Keep it local for every file directive, including directory/any.
|
|
1305
|
+
local -a expl=()
|
|
393
1306
|
# Use zsh's native file completion
|
|
394
1307
|
case "\$type" in
|
|
395
|
-
file)
|
|
396
|
-
if [[ -n "\$
|
|
397
|
-
#
|
|
398
|
-
#
|
|
399
|
-
#
|
|
400
|
-
|
|
401
|
-
|
|
1308
|
+
file|any)
|
|
1309
|
+
if [[ "\$type" == "file" || -n "\$__initial_file_pattern" ]]; then
|
|
1310
|
+
# Regular-file suggestions retain symlinked files; initial
|
|
1311
|
+
# any globs use a non-directory qualifier instead.
|
|
1312
|
+
local file_pattern="\${ext_pattern:-*}(#q-.)"
|
|
1313
|
+
if [[ "\$__fixed_glob" == "1" ]]; then
|
|
1314
|
+
file_pattern="\${ext_pattern:-*}(#q-.e:${fixedGlobFunction}:)"
|
|
1315
|
+
fi
|
|
1316
|
+
if [[ -n "\$__initial_file_pattern" ]]; then
|
|
1317
|
+
local __file_qualifier="-."
|
|
1318
|
+
[[ "\$type" == "any" ]] && __file_qualifier="^-/"
|
|
1319
|
+
local -a __initial_file_patterns=()
|
|
1320
|
+
if [[ -n "\$extensions" ]]; then
|
|
1321
|
+
setopt extended_glob
|
|
1322
|
+
local __initial_extension
|
|
1323
|
+
for __initial_extension in "\${(@s:,:)extensions}"; do
|
|
1324
|
+
__initial_file_patterns+=("(\${__initial_file_pattern}~^*.\${__initial_extension})(#q\${__file_qualifier})")
|
|
1325
|
+
done
|
|
1326
|
+
else
|
|
1327
|
+
__initial_file_patterns=("\${__initial_file_pattern}(#q\${__file_qualifier})")
|
|
1328
|
+
fi
|
|
1329
|
+
file_pattern="\${(j: :)__initial_file_patterns}"
|
|
1330
|
+
fi
|
|
1331
|
+
local file_pattern_prefix="\$PREFIX"
|
|
1332
|
+
local -a match mbegin mend
|
|
1333
|
+
# Native qualifier copying concatenates flags with the explicit
|
|
1334
|
+
# regular-file group. Keep backend attributes in their own group.
|
|
1335
|
+
if [[ -n "\$compstate[pattern_match]" && -z "\$SUFFIX" ]]; then
|
|
1336
|
+
local file_qualifier file_qualifiers=""
|
|
1337
|
+
while _have_glob_qual "\$file_pattern_prefix" complete; do
|
|
1338
|
+
file_qualifier="\${match[5]#\\#q}"
|
|
1339
|
+
file_pattern_prefix="\$match[2]"
|
|
1340
|
+
file_qualifiers="(#q\$file_qualifier)\$file_qualifiers"
|
|
1341
|
+
done
|
|
1342
|
+
if [[ -n "\$file_qualifiers" ]]; then
|
|
1343
|
+
file_pattern+="\$file_qualifiers"
|
|
1344
|
+
fi
|
|
1345
|
+
fi
|
|
1346
|
+
local file_subtree_pattern="**/*(#q/N)"
|
|
1347
|
+
local -a file_subtree=() file_recursive_patterns=()
|
|
1348
|
+
local -a file_tags=(files directories)
|
|
1349
|
+
local file_recursive_pattern file_directory file_prefix file_tag
|
|
1350
|
+
if [[ -n "\$__initial_file_pattern" || -z "\$ext_pattern" ]] &&
|
|
1351
|
+
zstyle -t ":completion:\${curcontext}:" list-dirs-first; then
|
|
1352
|
+
file_tags=(directories files)
|
|
1353
|
+
fi
|
|
1354
|
+
_tags "\${file_tags[@]}"
|
|
1355
|
+
while _tags; do
|
|
1356
|
+
local file_matched=0
|
|
1357
|
+
for file_tag in "\${file_tags[@]}"; do
|
|
1358
|
+
if [[ "\$file_tag" == directories ]]; then
|
|
1359
|
+
local file_tag_prefix="\$PREFIX"
|
|
1360
|
+
PREFIX="\$file_pattern_prefix"
|
|
1361
|
+
while _next_label directories expl directory; do
|
|
1362
|
+
# File qualifiers constrain final results, not the
|
|
1363
|
+
# directories needed to navigate to those results.
|
|
1364
|
+
_path_files -/ "\${expl[@]}" && file_matched=1
|
|
1365
|
+
done
|
|
1366
|
+
PREFIX="\$file_tag_prefix"
|
|
1367
|
+
else
|
|
1368
|
+
local file_tag_prefix="\$PREFIX"
|
|
1369
|
+
PREFIX="\$file_pattern_prefix"
|
|
1370
|
+
while _next_label files expl file; do
|
|
1371
|
+
local -a file_ignore=() file_ignore_args=()
|
|
1372
|
+
# Explicit globs bypass automatic fignore. Apply it only
|
|
1373
|
+
# when the tag has not supplied its own ignore patterns.
|
|
1374
|
+
if [[ -z "\$__initial_file_pattern" && -z "\$ext_pattern" && ( "\$__fixed_glob" != "1" || "\$pattern" == "*" ) && -z "\$_comp_no_ignore" && -n "\$FIGNORE" ]] && (( \${#_comp_ignore[@]} == 0 )); then
|
|
1375
|
+
file_ignore=("?*\${^fignore[@]}")
|
|
1376
|
+
file_ignore_args=(-F file_ignore)
|
|
1377
|
+
fi
|
|
1378
|
+
if _path_files -g "\${file_pattern}" "\${expl[@]}" "\${file_ignore_args[@]}"; then
|
|
1379
|
+
file_matched=1
|
|
1380
|
+
elif [[ ( -n "\$__initial_file_pattern" || -z "\$ext_pattern" ) && "\$PREFIX\$SUFFIX" != */* ]] &&
|
|
1381
|
+
zstyle -a ":completion:\${curcontext}:files" recursive-files file_recursive_patterns; then
|
|
1382
|
+
# Match _files' recursive fallback, retaining the same
|
|
1383
|
+
# file filter and tag-specific ignore options.
|
|
1384
|
+
for file_recursive_pattern in "\${file_recursive_patterns[@]}"; do
|
|
1385
|
+
if [[ "\$PWD/" = \${~file_recursive_pattern} ]]; then
|
|
1386
|
+
if (( \${#file_subtree[@]} == 0 )); then
|
|
1387
|
+
file_subtree=(\${~file_subtree_pattern})
|
|
1388
|
+
fi
|
|
1389
|
+
for file_directory in "\${file_subtree[@]}"; do
|
|
1390
|
+
file_prefix="\$PREFIX"
|
|
1391
|
+
PREFIX="\$file_directory/\$PREFIX"
|
|
1392
|
+
_path_files -g "\${file_pattern}" "\${expl[@]}" "\${file_ignore_args[@]}" && file_matched=1
|
|
1393
|
+
PREFIX="\$file_prefix"
|
|
1394
|
+
done
|
|
1395
|
+
break
|
|
1396
|
+
fi
|
|
1397
|
+
done
|
|
1398
|
+
fi
|
|
1399
|
+
done
|
|
1400
|
+
PREFIX="\$file_tag_prefix"
|
|
1401
|
+
fi
|
|
1402
|
+
done
|
|
1403
|
+
(( file_matched )) && break
|
|
1404
|
+
done
|
|
1405
|
+
elif [[ -n "\$ext_pattern" || "\$__fixed_glob" == "1" ]]; then
|
|
1406
|
+
local file_pattern="\${ext_pattern}(#q^-/)"
|
|
1407
|
+
if [[ "\$__fixed_glob" == "1" ]]; then
|
|
1408
|
+
# Put the predicate before ^, which negates later qualifiers.
|
|
1409
|
+
file_pattern="\${ext_pattern:-*}(#qe:${fixedGlobFunction}:^-/)"
|
|
1410
|
+
fi
|
|
402
1411
|
_wanted files expl file _path_files -g "\${file_pattern}"
|
|
403
1412
|
_wanted directories expl directory _path_files -/
|
|
404
1413
|
else
|
|
@@ -406,22 +1415,25 @@ function _${programName.replace(/[^a-zA-Z0-9]/g, "_")} () {
|
|
|
406
1415
|
fi
|
|
407
1416
|
;;
|
|
408
1417
|
directory)
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
if [[ -n "\$ext_pattern" ]]; then
|
|
413
|
-
local file_pattern="\${ext_pattern}(#q^-/)"
|
|
414
|
-
_wanted files expl file _path_files -g "\${file_pattern}"
|
|
415
|
-
_wanted directories expl directory _path_files -/
|
|
1418
|
+
if [[ -n "\$__initial_file_pattern" ]]; then
|
|
1419
|
+
# -/ would add every directory alongside the filtered glob.
|
|
1420
|
+
_wanted directories expl directory _path_files -g "\${__initial_file_pattern}(#q-/)"
|
|
416
1421
|
else
|
|
417
|
-
|
|
1422
|
+
_directories
|
|
418
1423
|
fi
|
|
419
1424
|
;;
|
|
420
1425
|
esac
|
|
421
|
-
|
|
422
1426
|
# Restore PREFIX and glob_dots to their previous state
|
|
423
1427
|
PREFIX="\$__saved_prefix"
|
|
1428
|
+
IPREFIX="\$__saved_iprefix"
|
|
1429
|
+
current="\$__saved_current"
|
|
1430
|
+
__logical_prefix="\$__saved_logical_prefix"
|
|
1431
|
+
if [[ "\$__was_extended_glob" == "1" ]]; then setopt extended_glob; else unsetopt extended_glob; fi
|
|
1432
|
+
if (( __pattern_match_changed )); then
|
|
1433
|
+
compstate[pattern_match]="\$__saved_pattern_match"
|
|
1434
|
+
fi
|
|
424
1435
|
if [[ "\$__was_sh_glob" == "1" ]]; then setopt sh_glob; else unsetopt sh_glob; fi
|
|
1436
|
+
if [[ "\$__was_sh_word_split" == "1" ]]; then setopt sh_word_split; else unsetopt sh_word_split; fi
|
|
425
1437
|
if [[ "\$__was_glob_dots" == "1" ]]; then setopt glob_dots; else unsetopt glob_dots; fi
|
|
426
1438
|
else
|
|
427
1439
|
# Regular literal completion
|
|
@@ -438,32 +1450,52 @@ function _${programName.replace(/[^a-zA-Z0-9]/g, "_")} () {
|
|
|
438
1450
|
# Prepare completion with descriptions for _describe
|
|
439
1451
|
local -a matches
|
|
440
1452
|
local -i i
|
|
1453
|
+
local __match
|
|
441
1454
|
for (( i=1; i <= \${#completions[@]}; i++ )); do
|
|
1455
|
+
# _describe uses backslashes to escape its colon separator.
|
|
1456
|
+
__match="\${completions[i]//\\\\/\\\\\\\\}"
|
|
1457
|
+
__match="\${__match//:/\\\\:}"
|
|
442
1458
|
if [[ -n "\${descriptions[i]}" ]]; then
|
|
443
|
-
matches+=("\$
|
|
1459
|
+
matches+=("\$__match:\${descriptions[i]}")
|
|
444
1460
|
else
|
|
445
|
-
matches+=("\$
|
|
1461
|
+
matches+=("\$__match")
|
|
446
1462
|
fi
|
|
447
1463
|
done
|
|
448
|
-
|
|
1464
|
+
if [[ -n "\$QISUFFIX" ]]; then
|
|
1465
|
+
# A closing quote already exists; do not append a space inside it.
|
|
1466
|
+
_describe 'commands' matches -S ''
|
|
1467
|
+
else
|
|
1468
|
+
_describe 'commands' matches
|
|
1469
|
+
fi
|
|
449
1470
|
fi
|
|
450
1471
|
}
|
|
451
1472
|
|
|
1473
|
+
function ${fixedGlobFunction} () {
|
|
1474
|
+
local __fixed_candidate="\${REPLY:a}"
|
|
1475
|
+
(( \${__fixed_matches[(Ie)\$__fixed_candidate]} ))
|
|
1476
|
+
}
|
|
1477
|
+
|
|
452
1478
|
if (( $+functions[compdef] )); then
|
|
453
|
-
compdef
|
|
1479
|
+
compdef ${functionName} ${programName}
|
|
1480
|
+
fi
|
|
1481
|
+
|
|
1482
|
+
# An autoloaded file runs as a function body. Dispatch its initial request;
|
|
1483
|
+
# sourcing the file only defines and registers the completion helper.
|
|
1484
|
+
if [[ \$ZSH_EVAL_CONTEXT == *:loadautofunc || \$ZSH_EVAL_CONTEXT == *:shfunc ]]; then
|
|
1485
|
+
${functionName} "\$@"
|
|
454
1486
|
fi
|
|
455
1487
|
`;
|
|
456
1488
|
},
|
|
457
1489
|
*encodeSuggestions(suggestions) {
|
|
458
1490
|
for (const suggestion of suggestions) if (suggestion.kind === "literal") {
|
|
459
1491
|
const description = suggestion.description == null ? "" : sanitizeForTransport(formatMessage(suggestion.description, { colors: false }));
|
|
460
|
-
yield `${
|
|
1492
|
+
yield `${encodeLiteral(suggestion.text)}\0${description}\0`;
|
|
461
1493
|
} else {
|
|
462
1494
|
const extensions = encodeExtensions(suggestion.extensions);
|
|
463
1495
|
const hidden = suggestion.includeHidden ? "1" : "0";
|
|
464
1496
|
const description = suggestion.description == null ? "" : sanitizeForTransport(formatMessage(suggestion.description, { colors: false }));
|
|
465
1497
|
const pattern = encodePattern(suggestion.pattern ?? "");
|
|
466
|
-
yield `__FILE__:${suggestion.type}:${extensions}:${pattern}:${hidden}\0${description}\0`;
|
|
1498
|
+
yield `__FILE__:${suggestion.type}:${extensions}:${pattern}:${hidden}${encodeFilePrefix(suggestion)}\0${description}\0`;
|
|
467
1499
|
}
|
|
468
1500
|
}
|
|
469
1501
|
};
|
|
@@ -476,12 +1508,35 @@ const fish = {
|
|
|
476
1508
|
generateScript(programName, args = []) {
|
|
477
1509
|
validateProgramName(programName);
|
|
478
1510
|
const escapedArgs = args.map((arg) => `'${arg.replace(/'/g, "\\'")}'`).join(" ");
|
|
479
|
-
const functionName = `__${programName
|
|
1511
|
+
const functionName = `__${programName}_complete`;
|
|
480
1512
|
return `
|
|
481
1513
|
function ${functionName}
|
|
482
1514
|
set -l tokens (commandline -poc)
|
|
483
1515
|
set -l current (commandline -ct)
|
|
484
1516
|
|
|
1517
|
+
# commandline -poc decodes previous tokens, but commandline -ct returns
|
|
1518
|
+
# the current token as typed. Decode its quotes and escapes as well.
|
|
1519
|
+
# Keep the token as typed if decoding fails or yields a newline or tab,
|
|
1520
|
+
# which completion output cannot carry.
|
|
1521
|
+
set -l __raw_current "$current"
|
|
1522
|
+
set -l __decoded (string unescape -- "$current")
|
|
1523
|
+
or begin
|
|
1524
|
+
# string unescape rejects an unfinished trailing backslash. fish
|
|
1525
|
+
# keeps it literally inside single quotes, which also keep a " as
|
|
1526
|
+
# is, and otherwise waits for the character it escapes.
|
|
1527
|
+
set -l __prefix (string replace -r '\\\\\\\\$' '' -- "$current")
|
|
1528
|
+
set -l __probe (string unescape -- "$__prefix\\"")
|
|
1529
|
+
set -l __plain (string unescape -- "$__prefix")
|
|
1530
|
+
if test "$__probe" = "$__plain\\""
|
|
1531
|
+
set __decoded (string unescape -- "$current\\\\")
|
|
1532
|
+
else
|
|
1533
|
+
set __decoded (string unescape -- "$__prefix")
|
|
1534
|
+
end
|
|
1535
|
+
end
|
|
1536
|
+
and test (count $__decoded) -eq 1
|
|
1537
|
+
and not string match -qr '\\t' -- "$__decoded"
|
|
1538
|
+
and set current "$__decoded"
|
|
1539
|
+
|
|
485
1540
|
# Extract previous arguments (skip the command name)
|
|
486
1541
|
set -l prev
|
|
487
1542
|
set -l count (count $tokens)
|
|
@@ -493,7 +1548,12 @@ function ${functionName}
|
|
|
493
1548
|
${escapedArgs ? ` set -l output (${programName} ${escapedArgs} $prev $current 2>/dev/null)\n` : ` set -l output (${programName} $prev $current 2>/dev/null)\n`}
|
|
494
1549
|
# Process each line of output
|
|
495
1550
|
for line in $output
|
|
496
|
-
|
|
1551
|
+
set -l literal 0
|
|
1552
|
+
if string match -q '__LITERAL__:*' -- $line
|
|
1553
|
+
set line (string sub -s 13 -- $line)
|
|
1554
|
+
set literal 1
|
|
1555
|
+
end
|
|
1556
|
+
if test $literal -eq 0; and string match -q '__FILE__:*' -- $line
|
|
497
1557
|
# Parse file completion directive: __FILE__:type:extensions:pattern:hidden
|
|
498
1558
|
set -l directive (string split \\t -- $line)[1]
|
|
499
1559
|
set -l parts (string split ':' -- $directive)
|
|
@@ -501,6 +1561,34 @@ ${escapedArgs ? ` set -l output (${programName} ${escapedArgs} $prev $current
|
|
|
501
1561
|
set -l extensions $parts[3]
|
|
502
1562
|
set -l pattern (string replace -a '%25' '%' -- (string replace -a '%3A' ':' -- $parts[4]))
|
|
503
1563
|
set -l hidden $parts[5]
|
|
1564
|
+
set -l insertion_prefix ""
|
|
1565
|
+
if test (count $parts) -ge 6
|
|
1566
|
+
set insertion_prefix (string replace -a '%25' '%' -- (string replace -a '%3A' ':' -- $parts[6]))
|
|
1567
|
+
end
|
|
1568
|
+
set -l saved_current "$current"
|
|
1569
|
+
set -l saved_raw_current "$__raw_current"
|
|
1570
|
+
set -l prefix_length (string length -- "$insertion_prefix")
|
|
1571
|
+
if test $prefix_length -gt 0
|
|
1572
|
+
and test (string sub -l $prefix_length -- "$pattern") = "$insertion_prefix"
|
|
1573
|
+
and test (string sub -l $prefix_length -- "$current") = "$insertion_prefix"
|
|
1574
|
+
set -l start (math $prefix_length + 1)
|
|
1575
|
+
set pattern (string sub -s $start -- "$pattern")
|
|
1576
|
+
set current (string sub -s $start -- "$current")
|
|
1577
|
+
# Keep the raw word aligned so an unquoted key is not mistaken
|
|
1578
|
+
# for quoting when comparing it with the decoded value.
|
|
1579
|
+
if test (string sub -l $prefix_length -- "$__raw_current") = "$insertion_prefix"
|
|
1580
|
+
set __raw_current (string sub -s $start -- "$__raw_current")
|
|
1581
|
+
end
|
|
1582
|
+
else
|
|
1583
|
+
set insertion_prefix ""
|
|
1584
|
+
end
|
|
1585
|
+
|
|
1586
|
+
# path() echoes the current word as its pattern. When that word
|
|
1587
|
+
# was quoted or escaped, treat the echo as the typed word rather
|
|
1588
|
+
# than as a pattern from the program.
|
|
1589
|
+
if test "$current" != "$__raw_current"; and test "$pattern" = "$current"
|
|
1590
|
+
set pattern ""
|
|
1591
|
+
end
|
|
504
1592
|
|
|
505
1593
|
# When a pattern is specified, use it as the glob base instead
|
|
506
1594
|
# of the current word. If the user has already typed beyond the
|
|
@@ -513,15 +1601,13 @@ ${escapedArgs ? ` set -l output (${programName} ${escapedArgs} $prev $current
|
|
|
513
1601
|
# Normalize leading ./ so that ./src/ma matches pattern src/
|
|
514
1602
|
set -l __norm_current (string replace -r '^\\./' '' -- "$current")
|
|
515
1603
|
set -l __norm_pattern (string replace -r '^\\./' '' -- "$pattern")
|
|
516
|
-
# For wildcard patterns, compare only the directory prefix
|
|
1604
|
+
# For wildcard patterns, compare only the directory prefix.
|
|
1605
|
+
# Use a regex: fish wildcards have no character classes.
|
|
1606
|
+
# Only the basename can hold wildcards; a ? or * in a
|
|
1607
|
+
# directory name such as What?/ is literal.
|
|
517
1608
|
set -l __compare_pattern "$__norm_pattern"
|
|
518
|
-
if string match -
|
|
519
|
-
set __compare_pattern (string replace -r '[
|
|
520
|
-
if string match -q '*/*' -- "$__compare_pattern"
|
|
521
|
-
set __compare_pattern (string replace -r '/[^/]*$' '/' -- "$__compare_pattern")
|
|
522
|
-
else
|
|
523
|
-
set __compare_pattern ""
|
|
524
|
-
end
|
|
1609
|
+
if string match -qr '[*?][^/]*$' -- "$__compare_pattern"
|
|
1610
|
+
set __compare_pattern (string replace -r '[^/]*$' '' -- "$__compare_pattern")
|
|
525
1611
|
end
|
|
526
1612
|
set -l __cp_len (string length -- "$__compare_pattern")
|
|
527
1613
|
set -l __nc_len (string length -- "$__norm_current")
|
|
@@ -536,14 +1622,17 @@ ${escapedArgs ? ` set -l output (${programName} ${escapedArgs} $prev $current
|
|
|
536
1622
|
end
|
|
537
1623
|
end
|
|
538
1624
|
|
|
539
|
-
# Expand tilde prefix for globbing—fish does not expand ~
|
|
540
|
-
# inside variable substitutions, so replace it with $HOME
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
1625
|
+
# Expand tilde prefix for globbing — fish does not expand ~
|
|
1626
|
+
# inside variable substitutions, so replace it with $HOME. Like
|
|
1627
|
+
# fish itself, keep a quoted or escaped tilde in the word literal.
|
|
1628
|
+
if test $__from_pattern -eq 1; or string match -q '~*' -- "$__raw_current"
|
|
1629
|
+
if string match -q '~/*' -- "$glob_base"
|
|
1630
|
+
set __tilde_prefix "~"
|
|
1631
|
+
set glob_base (string replace -r '^~' "$HOME" -- "$glob_base")
|
|
1632
|
+
else if string match -q '~' -- "$glob_base"
|
|
1633
|
+
set __tilde_prefix "~"
|
|
1634
|
+
set glob_base "$HOME/"
|
|
1635
|
+
end
|
|
547
1636
|
end
|
|
548
1637
|
|
|
549
1638
|
# If the glob base is a directory without a trailing slash,
|
|
@@ -553,54 +1642,82 @@ ${escapedArgs ? ` set -l output (${programName} ${escapedArgs} $prev $current
|
|
|
553
1642
|
end
|
|
554
1643
|
|
|
555
1644
|
# Pre-expand file candidates. When the glob base came from
|
|
556
|
-
# the program's pattern and contains * or ?, use
|
|
557
|
-
# complete glob expression. Otherwise append * as a
|
|
1645
|
+
# the program's pattern and its basename contains * or ?, use
|
|
1646
|
+
# it as the complete glob expression. Otherwise append * as a
|
|
1647
|
+
# prefix, so a literal ? in a directory name such as What?/
|
|
1648
|
+
# still lists that directory. A pattern equal to the current
|
|
1649
|
+
# word is the typed text echoed back (as path() does), so its
|
|
1650
|
+
# ? and * are literal characters, not wildcards.
|
|
558
1651
|
# Note: [ is NOT treated as a glob because it commonly
|
|
559
1652
|
# appears in literal filenames like [draft] or foo[1].txt.
|
|
560
1653
|
set -l __has_glob 0
|
|
561
1654
|
if test $__from_pattern -eq 1
|
|
562
|
-
and string match -
|
|
1655
|
+
and string match -qr '[*?][^/]*$' -- "$glob_base"
|
|
1656
|
+
and test "$pattern" != "$current"
|
|
563
1657
|
set __has_glob 1
|
|
564
1658
|
end
|
|
565
1659
|
set -l __candidates
|
|
566
1660
|
if test $__has_glob -eq 1
|
|
567
1661
|
# Safe glob expansion without eval: split the pattern into
|
|
568
|
-
# directory and filter
|
|
569
|
-
#
|
|
570
|
-
|
|
1662
|
+
# a directory prefix and a basename filter, list the
|
|
1663
|
+
# directory, then match basenames against a regex built
|
|
1664
|
+
# from the filter. The prefix keeps one empty element for
|
|
1665
|
+
# bare patterns so that $__glob_prefix* still expands.
|
|
1666
|
+
set -l __glob_prefix ""
|
|
1667
|
+
if string match -q '*/*' -- "$glob_base"
|
|
1668
|
+
set __glob_prefix (string replace -r '[^/]*$' '' -- "$glob_base")
|
|
1669
|
+
end
|
|
571
1670
|
set -l __glob_filter (string replace -r '.*/' '' -- "$glob_base")
|
|
572
|
-
|
|
573
|
-
|
|
1671
|
+
# Translate the filter into an anchored regex: * and ? are
|
|
1672
|
+
# wildcards and every other character is literal. Fish's
|
|
1673
|
+
# own wildcards cannot be used here because ? is literal
|
|
1674
|
+
# when the qmark-noglob feature is enabled (fish 4 default).
|
|
1675
|
+
set -l __glob_regex '^'
|
|
1676
|
+
for __ch in (string split '' -- "$__glob_filter")
|
|
1677
|
+
if test "$__ch" = '*'
|
|
1678
|
+
set __glob_regex "$__glob_regex.*"
|
|
1679
|
+
else if test "$__ch" = '?'
|
|
1680
|
+
set __glob_regex "$__glob_regex."
|
|
1681
|
+
else
|
|
1682
|
+
set __glob_regex "$__glob_regex"(string escape --style=regex -- "$__ch")
|
|
1683
|
+
end
|
|
574
1684
|
end
|
|
1685
|
+
set __glob_regex "$__glob_regex"'$'
|
|
575
1686
|
# Match files by the glob filter. Fish's * does not match
|
|
576
|
-
# dotfiles, so also scan .* when the filter targets them
|
|
577
|
-
|
|
1687
|
+
# dotfiles, so also scan .* when the filter targets them or
|
|
1688
|
+
# hidden files are requested.
|
|
1689
|
+
set -l __entries $__glob_prefix*
|
|
1690
|
+
if test "$hidden" = "1"; or string match -q '.*' -- "$__glob_filter"
|
|
1691
|
+
set -a __entries $__glob_prefix.*
|
|
1692
|
+
end
|
|
1693
|
+
for __item in $__entries
|
|
578
1694
|
if test -e "$__item"
|
|
579
|
-
set -l __bn (
|
|
580
|
-
if
|
|
581
|
-
|
|
1695
|
+
set -l __bn (string replace -r '.*/' '' -- "$__item")
|
|
1696
|
+
if contains -- "$__bn" . ..
|
|
1697
|
+
continue
|
|
582
1698
|
end
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
if string match -q '.*' -- "$__glob_filter"
|
|
586
|
-
for __item in $__glob_dir/.*
|
|
587
|
-
if test -e "$__item"
|
|
588
|
-
set -l __bn (basename "$__item")
|
|
589
|
-
if test "$__bn" = "." -o "$__bn" = ".."
|
|
590
|
-
continue
|
|
591
|
-
end
|
|
592
|
-
if string match -q "$__glob_filter" -- "$__bn"
|
|
593
|
-
set -a __candidates "$__item"
|
|
594
|
-
end
|
|
1699
|
+
if string match -qr -- "$__glob_regex" "$__bn"
|
|
1700
|
+
set -a __candidates "$__item"
|
|
595
1701
|
end
|
|
596
1702
|
end
|
|
597
1703
|
end
|
|
598
1704
|
# For file/any modes, also include directories for navigation.
|
|
599
1705
|
# Skip for directory mode so the pattern filter is respected.
|
|
1706
|
+
# Directories the filter already matched are not repeated.
|
|
600
1707
|
if test "$type" != "directory"
|
|
601
|
-
|
|
1708
|
+
set -l __nav_dirs $__glob_prefix*/
|
|
1709
|
+
if test "$hidden" = "1"
|
|
1710
|
+
set -a __nav_dirs $__glob_prefix.*/
|
|
1711
|
+
end
|
|
1712
|
+
for __item in $__nav_dirs
|
|
1713
|
+
set __item (string replace -r '/$' '' -- "$__item")
|
|
1714
|
+
set -l __bn (string replace -r '.*/' '' -- "$__item")
|
|
1715
|
+
if contains -- "$__bn" . ..
|
|
1716
|
+
continue
|
|
1717
|
+
end
|
|
602
1718
|
if test -d "$__item"
|
|
603
|
-
|
|
1719
|
+
and not string match -qr -- "$__glob_regex" "$__bn"
|
|
1720
|
+
set -a __candidates "$__item"
|
|
604
1721
|
end
|
|
605
1722
|
end
|
|
606
1723
|
end
|
|
@@ -703,7 +1820,7 @@ ${escapedArgs ? ` set -l output (${programName} ${escapedArgs} $prev $current
|
|
|
703
1820
|
if test "$hidden" != "1" -a (string sub -l 1 -- $__glob_basename) != "."
|
|
704
1821
|
set -l filtered
|
|
705
1822
|
for item in $items
|
|
706
|
-
set -l basename (basename $item)
|
|
1823
|
+
set -l basename (basename -- "$item")
|
|
707
1824
|
if not string match -q '.*' -- $basename
|
|
708
1825
|
set -a filtered $item
|
|
709
1826
|
end
|
|
@@ -720,10 +1837,13 @@ ${escapedArgs ? ` set -l output (${programName} ${escapedArgs} $prev $current
|
|
|
720
1837
|
set items $restored
|
|
721
1838
|
end
|
|
722
1839
|
|
|
723
|
-
# Output file completions
|
|
1840
|
+
# Output file completions. Use printf rather than echo, which
|
|
1841
|
+
# would take filenames like -n or -e as its own options.
|
|
724
1842
|
for item in $items
|
|
725
|
-
|
|
1843
|
+
printf '%s\\n' "$insertion_prefix$item"
|
|
726
1844
|
end
|
|
1845
|
+
set current "$saved_current"
|
|
1846
|
+
set __raw_current "$saved_raw_current"
|
|
727
1847
|
else
|
|
728
1848
|
# Regular literal completion - split by tab
|
|
729
1849
|
set -l parts (string split \\t -- $line)
|
|
@@ -747,19 +1867,29 @@ complete -c ${programName} -f -a '(${functionName})'
|
|
|
747
1867
|
if (i > 0) yield "\n";
|
|
748
1868
|
if (suggestion.kind === "literal") {
|
|
749
1869
|
const description = suggestion.description == null ? "" : sanitizeForTransport(formatMessage(suggestion.description, { colors: false }));
|
|
750
|
-
yield `${
|
|
1870
|
+
yield `${encodeLiteral(suggestion.text)}\t${description}`;
|
|
751
1871
|
} else {
|
|
752
1872
|
const extensions = encodeExtensions(suggestion.extensions);
|
|
753
1873
|
const hidden = suggestion.includeHidden ? "1" : "0";
|
|
754
1874
|
const description = suggestion.description == null ? "" : sanitizeForTransport(formatMessage(suggestion.description, { colors: false }));
|
|
755
1875
|
const pattern = encodePattern(suggestion.pattern ?? "");
|
|
756
|
-
yield `__FILE__:${suggestion.type}:${extensions}:${pattern}:${hidden}\t${description}`;
|
|
1876
|
+
yield `__FILE__:${suggestion.type}:${extensions}:${pattern}:${hidden}${encodeFilePrefix(suggestion)}\t${description}`;
|
|
757
1877
|
}
|
|
758
1878
|
i++;
|
|
759
1879
|
}
|
|
760
1880
|
}
|
|
761
1881
|
};
|
|
762
1882
|
/**
|
|
1883
|
+
* Quotes a string as a Nushell string literal. Nushell single-quoted strings
|
|
1884
|
+
* have no escape sequences, so a value containing an apostrophe is written as
|
|
1885
|
+
* a double-quoted string, in which only backslashes and double quotes need
|
|
1886
|
+
* escaping.
|
|
1887
|
+
*/
|
|
1888
|
+
function quoteNuString(value) {
|
|
1889
|
+
if (!value.includes("'")) return `'${value}'`;
|
|
1890
|
+
return `"${value.replace(/["\\]/g, "\\$&")}"`;
|
|
1891
|
+
}
|
|
1892
|
+
/**
|
|
763
1893
|
* The Nushell completion generator.
|
|
764
1894
|
* @since 0.6.0
|
|
765
1895
|
*/
|
|
@@ -767,66 +1897,193 @@ const nu = {
|
|
|
767
1897
|
name: "nu",
|
|
768
1898
|
generateScript(programName, args = []) {
|
|
769
1899
|
validateProgramName(programName);
|
|
770
|
-
const escapedArgs = args.map(
|
|
1900
|
+
const escapedArgs = args.map(quoteNuString).join(" ");
|
|
771
1901
|
const safeName = programName.replace(/[^a-zA-Z0-9]+/g, "-");
|
|
772
1902
|
const functionName = `nu-complete-${safeName}`;
|
|
773
1903
|
return `
|
|
774
|
-
# Helper to split
|
|
1904
|
+
# Helper to split a command line into the arguments Nushell would pass to
|
|
1905
|
+
# an external command. Words are found the way Nushell's lexer finds them
|
|
1906
|
+
# (quotes and raw strings may contain whitespace), then each word is decoded
|
|
1907
|
+
# like an external argument. A final empty argument is added when the line
|
|
1908
|
+
# ends in unquoted whitespace, for the word about to be completed.
|
|
775
1909
|
def args-split []: string -> list<string> {
|
|
776
|
-
let STATE_NORMAL = 0
|
|
777
|
-
let STATE_IN_SINGLE_QUOTE = 1
|
|
778
|
-
let STATE_IN_DOUBLE_QUOTE = 2
|
|
779
|
-
let STATE_ESCAPE = 3
|
|
780
1910
|
let WHITESPACES = [" " "\\t" "\\n" "\\r"]
|
|
1911
|
+
let QUOTES = ["'" '"' "\`"]
|
|
1912
|
+
|
|
1913
|
+
# Decodes one word. An unterminated word is the one still being typed,
|
|
1914
|
+
# so its open quote is taken as if it were closed.
|
|
1915
|
+
let decode = {|word: string, unterminated: bool|
|
|
1916
|
+
let hashes = if ($word | str starts-with "r#") {
|
|
1917
|
+
$word | str replace -r '(?s)^r(#+).*$' '$1'
|
|
1918
|
+
} else {
|
|
1919
|
+
""
|
|
1920
|
+
}
|
|
1921
|
+
if ($hashes | is-not-empty) {
|
|
1922
|
+
let prefix = "r" + $hashes + "'"
|
|
1923
|
+
let suffix = "'" + $hashes
|
|
1924
|
+
let size = ($hashes | str length)
|
|
1925
|
+
if ($word | str starts-with $prefix) and ($word | str ends-with $suffix) and (($word | str length) >= ($size * 2 + 3)) {
|
|
1926
|
+
$word | str replace $prefix "" | str replace -r ($suffix + '$') ""
|
|
1927
|
+
} else if $unterminated and ($word | str starts-with $prefix) {
|
|
1928
|
+
$word | str replace $prefix ""
|
|
1929
|
+
} else {
|
|
1930
|
+
$word
|
|
1931
|
+
}
|
|
1932
|
+
} else {
|
|
1933
|
+
mut result = ""
|
|
1934
|
+
mut quote = ""
|
|
1935
|
+
mut quoted = ""
|
|
1936
|
+
mut escaped = false
|
|
1937
|
+
for char in ($word | split chars) {
|
|
1938
|
+
if $quote == "" {
|
|
1939
|
+
if ($char in $QUOTES) {
|
|
1940
|
+
$quote = $char
|
|
1941
|
+
$quoted = ""
|
|
1942
|
+
} else {
|
|
1943
|
+
$result = $result + $char
|
|
1944
|
+
}
|
|
1945
|
+
} else if $escaped {
|
|
1946
|
+
$quoted = $quoted + $char
|
|
1947
|
+
$escaped = false
|
|
1948
|
+
} else if $char == $quote {
|
|
1949
|
+
let body = $quoted
|
|
1950
|
+
$result = $result + (if $quote == '"' {
|
|
1951
|
+
try { '"' + $body + '"' | from nuon } catch { $body }
|
|
1952
|
+
} else {
|
|
1953
|
+
$body
|
|
1954
|
+
})
|
|
1955
|
+
$quote = ""
|
|
1956
|
+
} else {
|
|
1957
|
+
$quoted = $quoted + $char
|
|
1958
|
+
$escaped = ($quote == '"' and $char == '\\')
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
if $quote != "" {
|
|
1962
|
+
let body = $quoted
|
|
1963
|
+
$result = $result + (if not $unterminated {
|
|
1964
|
+
$quote + $body
|
|
1965
|
+
} else if $quote == '"' {
|
|
1966
|
+
try { '"' + $body + '"' | from nuon } catch { $body }
|
|
1967
|
+
} else {
|
|
1968
|
+
$body
|
|
1969
|
+
})
|
|
1970
|
+
}
|
|
1971
|
+
$result
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
781
1974
|
|
|
782
|
-
mut
|
|
783
|
-
mut
|
|
784
|
-
mut
|
|
785
|
-
mut
|
|
1975
|
+
mut words: list<string> = []
|
|
1976
|
+
mut word = ""
|
|
1977
|
+
mut in_word = false
|
|
1978
|
+
mut quote = ""
|
|
1979
|
+
mut escaped = false
|
|
1980
|
+
mut raw_hashes = ""
|
|
1981
|
+
mut raw_pending = ""
|
|
1982
|
+
mut prev = ""
|
|
1983
|
+
mut trailing_space = false
|
|
786
1984
|
|
|
787
1985
|
for char in ($in | split chars) {
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
if $char == "
|
|
796
|
-
$
|
|
797
|
-
} else if $char == '"
|
|
798
|
-
$
|
|
799
|
-
} else if ($char in $WHITESPACES) {
|
|
800
|
-
if ($current_token | is-not-empty) {
|
|
801
|
-
$result = $result | append $current_token
|
|
802
|
-
$current_token = ""
|
|
803
|
-
}
|
|
1986
|
+
$trailing_space = false
|
|
1987
|
+
if $quote == "#" and $char != "#" and $char != "'" {
|
|
1988
|
+
# Not a raw string after all; lex this character normally
|
|
1989
|
+
$quote = ""
|
|
1990
|
+
}
|
|
1991
|
+
if $quote == "r" {
|
|
1992
|
+
# Inside a raw string, which ends at ' followed by the same number of #
|
|
1993
|
+
if ($raw_pending | is-not-empty) and $char == "#" {
|
|
1994
|
+
$raw_pending = $raw_pending + $char
|
|
1995
|
+
} else if $char == "'" {
|
|
1996
|
+
$raw_pending = $char
|
|
804
1997
|
} else {
|
|
805
|
-
$
|
|
1998
|
+
$raw_pending = ""
|
|
1999
|
+
}
|
|
2000
|
+
if $raw_pending == ("'" + $raw_hashes) {
|
|
2001
|
+
$quote = ""
|
|
2002
|
+
$raw_pending = ""
|
|
806
2003
|
}
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
2004
|
+
$word = $word + $char
|
|
2005
|
+
} else if $quote == "#" {
|
|
2006
|
+
# Between r# and the opening ' of a raw string
|
|
2007
|
+
$word = $word + $char
|
|
2008
|
+
if $char == "#" {
|
|
2009
|
+
$raw_hashes = $raw_hashes + $char
|
|
810
2010
|
} else {
|
|
811
|
-
$
|
|
2011
|
+
$quote = "r"
|
|
812
2012
|
}
|
|
813
|
-
} else if $
|
|
814
|
-
|
|
815
|
-
|
|
2013
|
+
} else if $quote != "" {
|
|
2014
|
+
$word = $word + $char
|
|
2015
|
+
if $escaped {
|
|
2016
|
+
$escaped = false
|
|
2017
|
+
} else if $char == $quote {
|
|
2018
|
+
$quote = ""
|
|
816
2019
|
} else {
|
|
817
|
-
$
|
|
2020
|
+
$escaped = ($quote == '"' and $char == '\\')
|
|
818
2021
|
}
|
|
2022
|
+
} else if ($char in $WHITESPACES) {
|
|
2023
|
+
if $in_word {
|
|
2024
|
+
$words = $words | append (do $decode $word false)
|
|
2025
|
+
$word = ""
|
|
2026
|
+
$in_word = false
|
|
2027
|
+
}
|
|
2028
|
+
$trailing_space = true
|
|
2029
|
+
} else {
|
|
2030
|
+
if ($char in $QUOTES) {
|
|
2031
|
+
$quote = $char
|
|
2032
|
+
} else if $char == "#" and $prev == "r" {
|
|
2033
|
+
$quote = "#"
|
|
2034
|
+
$raw_hashes = "#"
|
|
2035
|
+
}
|
|
2036
|
+
$word = $word + $char
|
|
2037
|
+
$in_word = true
|
|
819
2038
|
}
|
|
2039
|
+
$prev = $char
|
|
2040
|
+
}
|
|
2041
|
+
if $in_word {
|
|
2042
|
+
$words = $words | append (do $decode $word ($quote != ""))
|
|
2043
|
+
} else if $trailing_space {
|
|
2044
|
+
$words = $words | append ""
|
|
820
2045
|
}
|
|
821
|
-
|
|
822
|
-
|
|
2046
|
+
$words
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
# Helper to tell whether Nushell would not read a value back verbatim as a
|
|
2050
|
+
# single argument. A leading ! is included because the line editor expands
|
|
2051
|
+
# history (e.g., !!) there. With --allow-expansion, a leading tilde and n-dot
|
|
2052
|
+
# path segments (e.g., ...) are allowed to expand.
|
|
2053
|
+
def needs-quote [--allow-expansion]: string -> bool {
|
|
2054
|
+
let value = $in
|
|
2055
|
+
let special = ["'" '"' '\`' "#" "(" ")" "[" "]" "{" "}" "|" ";" "&" "$" "*" "?" "<" ">"]
|
|
2056
|
+
(
|
|
2057
|
+
($value =~ '\\s')
|
|
2058
|
+
or ($special | any {|c| $value | str contains $c })
|
|
2059
|
+
or ($value =~ '^(\\+\\+|[-+*/])?=$')
|
|
2060
|
+
or ($value | str starts-with '!')
|
|
2061
|
+
or ((not $allow_expansion) and (
|
|
2062
|
+
($value | str starts-with '~')
|
|
2063
|
+
or ($value | split row -r '[/\\\\]' | any {|s| $s =~ '^\\.{3,}$' })
|
|
2064
|
+
))
|
|
2065
|
+
)
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
# Helper to quote a value so that Nushell reads it back verbatim as a
|
|
2069
|
+
# single argument. The line editor expands history at a ! that follows
|
|
2070
|
+
# whitespace even inside quotes, so such a ! is put between two quoted parts,
|
|
2071
|
+
# right after a quote, where it is left alone.
|
|
2072
|
+
def quote-arg []: string -> string {
|
|
2073
|
+
let value = $in
|
|
2074
|
+
if ($value | is-empty) or not ($value | needs-quote) {
|
|
2075
|
+
$value
|
|
2076
|
+
} else if ($value | str contains "'") {
|
|
2077
|
+
'"' + ($value | str replace -a '\\' '\\\\' | str replace -a '"' '\\"' | str replace -ar '(\\s)!' '\${1}"!"') + '"'
|
|
2078
|
+
} else {
|
|
2079
|
+
"'" + ($value | str replace -ar '(\\s)!' "\${1}'!'") + "'"
|
|
823
2080
|
}
|
|
824
|
-
$result
|
|
825
2081
|
}
|
|
826
2082
|
|
|
827
2083
|
# Completion command that calls back to the CLI
|
|
828
2084
|
export def "${functionName}" [context: string] {
|
|
829
|
-
# Split context into
|
|
2085
|
+
# Split context into the arguments Nushell would pass, including an empty
|
|
2086
|
+
# one for a new word when the context ends in unquoted whitespace
|
|
830
2087
|
let tokens = $context | args-split
|
|
831
2088
|
|
|
832
2089
|
# Remove the command name (first token) to get arguments
|
|
@@ -836,18 +2093,11 @@ export def "${functionName}" [context: string] {
|
|
|
836
2093
|
[]
|
|
837
2094
|
}
|
|
838
2095
|
|
|
839
|
-
# If context ends with whitespace, add empty string to represent the new argument
|
|
840
|
-
let args_with_empty = if ($context | str ends-with ' ') {
|
|
841
|
-
$args | append ''
|
|
842
|
-
} else {
|
|
843
|
-
$args
|
|
844
|
-
}
|
|
845
|
-
|
|
846
2096
|
# Ensure at least one argument (empty string) to get completions not script
|
|
847
|
-
let final_args = if ($
|
|
2097
|
+
let final_args = if ($args | is-empty) {
|
|
848
2098
|
['']
|
|
849
2099
|
} else {
|
|
850
|
-
$
|
|
2100
|
+
$args
|
|
851
2101
|
}
|
|
852
2102
|
|
|
853
2103
|
let output = try {
|
|
@@ -856,8 +2106,10 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
|
|
|
856
2106
|
}
|
|
857
2107
|
|
|
858
2108
|
# Process each line of output
|
|
859
|
-
$output | lines | each {|
|
|
860
|
-
|
|
2109
|
+
$output | lines | each {|raw_line|
|
|
2110
|
+
let literal = ($raw_line | str starts-with '__LITERAL__:')
|
|
2111
|
+
let line = if $literal { $raw_line | str substring 12.. } else { $raw_line }
|
|
2112
|
+
if (not $literal) and ($line | str starts-with '__FILE__:') {
|
|
861
2113
|
# Parse file completion directive: __FILE__:type:extensions:pattern:hidden
|
|
862
2114
|
let directive = ($line | split row "\t" | first)
|
|
863
2115
|
let parts = ($directive | split row ':')
|
|
@@ -866,6 +2118,10 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
|
|
|
866
2118
|
let pattern = ($parts | get 3 | str replace -a '%3A' ':' | str replace -a '%25' '%')
|
|
867
2119
|
let hidden = ($parts | get 4) == '1'
|
|
868
2120
|
|
|
2121
|
+
let insertion_prefix = if ($parts | length) > 5 {
|
|
2122
|
+
$parts | get 5 | str replace -a '%3A' ':' | str replace -a '%25' '%'
|
|
2123
|
+
} else { "" }
|
|
2124
|
+
|
|
869
2125
|
# Extract prefix from the last argument if it exists
|
|
870
2126
|
let prefix = if ($final_args | length) > 0 {
|
|
871
2127
|
$final_args | last
|
|
@@ -873,18 +2129,23 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
|
|
|
873
2129
|
""
|
|
874
2130
|
}
|
|
875
2131
|
|
|
2132
|
+
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 { "" }
|
|
2133
|
+
let prefix_length = ($insertion_prefix | str length)
|
|
2134
|
+
let pattern = if $prefix_length > 0 { $pattern | str substring $prefix_length.. } else { $pattern }
|
|
2135
|
+
let prefix = if $prefix_length > 0 { $prefix | str substring $prefix_length.. } else { $prefix }
|
|
2136
|
+
|
|
876
2137
|
# When a pattern is specified, use it as the glob base instead of
|
|
877
2138
|
# the user-typed prefix. If the user has already typed beyond the
|
|
878
2139
|
# pattern (e.g., pattern="src/" and prefix="src/ma"), keep the
|
|
879
2140
|
# prefix for incremental narrowing. Normalize path separators
|
|
880
2141
|
# before comparing so that Windows backslashes match forward slashes
|
|
881
2142
|
# in the transported pattern.
|
|
2143
|
+
let is_win = (($nu.os-info.name | str downcase) == "windows")
|
|
882
2144
|
let glob_base = if ($pattern | is-not-empty) {
|
|
883
2145
|
# Normalize separators and leading ./; downcase only on Windows
|
|
884
2146
|
# where filesystems are typically case-insensitive
|
|
885
2147
|
let norm_prefix_raw = ($prefix | str replace -a '\\' '/' | str replace -r '^\\./' '')
|
|
886
2148
|
let norm_pattern_raw = ($pattern | str replace -a '\\' '/' | str replace -r '^\\./' '')
|
|
887
|
-
let is_win = (($nu.os-info.name | str downcase) == "windows")
|
|
888
2149
|
let norm_prefix = (if $is_win { $norm_prefix_raw | str downcase } else { $norm_prefix_raw })
|
|
889
2150
|
let norm_pattern = (if $is_win { $norm_pattern_raw | str downcase } else { $norm_pattern_raw })
|
|
890
2151
|
# For wildcard patterns, compare only the directory prefix
|
|
@@ -903,10 +2164,27 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
|
|
|
903
2164
|
$prefix
|
|
904
2165
|
}
|
|
905
2166
|
|
|
2167
|
+
# Intersect fixed glob matches with the narrowed prefix candidates.
|
|
2168
|
+
# Directory-only suggestions retain their existing navigation behavior.
|
|
2169
|
+
let narrow_fixed_glob = ($type != "directory") and ($pattern =~ '[*?]') and ($prefix != $pattern) and ($glob_base == $prefix)
|
|
2170
|
+
let fixed_matches = if $narrow_fixed_glob {
|
|
2171
|
+
try {
|
|
2172
|
+
let fixed_pattern = ($pattern | into glob)
|
|
2173
|
+
let include_hidden = $hidden or ($prefix | path basename | str starts-with '.')
|
|
2174
|
+
let matches = if $include_hidden { ls -a --directory $fixed_pattern } else { ls --directory $fixed_pattern }
|
|
2175
|
+
$matches | each {|item|
|
|
2176
|
+
let name = ($env.PWD | path join $item.name | path expand --no-symlink)
|
|
2177
|
+
if $is_win { $name | str downcase } else { $name }
|
|
2178
|
+
}
|
|
2179
|
+
} catch { [] }
|
|
2180
|
+
} else { [] }
|
|
2181
|
+
|
|
906
2182
|
# If the glob base is a directory without a trailing slash (e.g., "src"),
|
|
907
2183
|
# append one so that ls enumerates its contents instead of matching
|
|
908
|
-
# siblings like "src-old"
|
|
909
|
-
|
|
2184
|
+
# siblings like "src-old". path expand follows symlinks, so that
|
|
2185
|
+
# a symlink to a directory is treated the same way; --no-symlink first
|
|
2186
|
+
# resolves .. against the logical $env.PWD as ls does
|
|
2187
|
+
let glob_base = if ($glob_base | is-not-empty) and ($glob_base | path expand --no-symlink | path expand | path type) == "dir" and (not ($glob_base | str ends-with "/")) {
|
|
910
2188
|
$glob_base + "/"
|
|
911
2189
|
} else {
|
|
912
2190
|
$glob_base
|
|
@@ -915,56 +2193,78 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
|
|
|
915
2193
|
# Generate file completions based on type.
|
|
916
2194
|
# When the glob base contains wildcard characters, use it as-is;
|
|
917
2195
|
# otherwise append * to treat it as a prefix.
|
|
918
|
-
# Note: into glob is required so that ls expands wildcards from a variable
|
|
2196
|
+
# Note: into glob is required so that ls expands wildcards from a variable.
|
|
2197
|
+
# A literal prefix has its brackets wrapped as [[] and []] so that path
|
|
2198
|
+
# text like "report[1]" is not parsed as a glob character class; a
|
|
2199
|
+
# wildcard pattern is a glob as a whole, so its brackets stay classes.
|
|
919
2200
|
let has_glob = ($glob_base =~ '[*?]')
|
|
920
2201
|
let ls_pattern = if ($glob_base | is-empty) {
|
|
921
2202
|
"."
|
|
922
2203
|
} else if $has_glob {
|
|
923
2204
|
($glob_base | into glob)
|
|
924
2205
|
} else {
|
|
925
|
-
($glob_base + "*" | into glob)
|
|
2206
|
+
(($glob_base | str replace -a -r '([\\[\\]])' '[$1]') + "*" | into glob)
|
|
926
2207
|
}
|
|
927
2208
|
|
|
928
2209
|
# When using a glob pattern, also compute a directory listing pattern
|
|
929
|
-
# so that file/any modes can include directories for navigation
|
|
2210
|
+
# so that file/any modes can include directories for navigation; its
|
|
2211
|
+
# directory part is a literal path
|
|
930
2212
|
let glob_dir_pattern = if $has_glob {
|
|
931
2213
|
let dir_part = ($glob_base | path dirname)
|
|
932
2214
|
let dir = if ($dir_part | is-empty) or ($dir_part == $glob_base) { "." } else { $dir_part }
|
|
933
|
-
($dir + "/*" | into glob)
|
|
2215
|
+
(($dir | str replace -a -r '([\\[\\]])' '[$1]') + "/*" | into glob)
|
|
934
2216
|
} else {
|
|
935
2217
|
null
|
|
936
2218
|
}
|
|
937
2219
|
|
|
938
|
-
#
|
|
2220
|
+
# List entries matching a pattern, using ls -a to include hidden
|
|
2221
|
+
# files when requested. ls reports every symlink as type "symlink",
|
|
2222
|
+
# so classify each one by its target's type instead (path expand
|
|
2223
|
+
# follows symlinks); broken or looping symlinks stay "symlink".
|
|
2224
|
+
# Join the name to $env.PWD first so that a leading ~ in an entry
|
|
2225
|
+
# name is not expanded to a home directory, and resolve .. against
|
|
2226
|
+
# the logical $env.PWD (--no-symlink) before following the symlink
|
|
2227
|
+
let list_entries = {|pattern|
|
|
2228
|
+
(if $hidden { ls -a $pattern } else { ls $pattern }) | each {|item|
|
|
2229
|
+
if $item.type == symlink {
|
|
2230
|
+
$item | update type ($env.PWD | path join $item.name | path expand --no-symlink | path expand | path type)
|
|
2231
|
+
} else {
|
|
2232
|
+
$item
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
|
|
939
2237
|
let items = try {
|
|
940
2238
|
match $type {
|
|
941
2239
|
"file" => {
|
|
942
2240
|
if ($extensions | is-empty) {
|
|
943
|
-
(
|
|
2241
|
+
(do $list_entries $ls_pattern) | where type == file or type == dir
|
|
944
2242
|
} else {
|
|
945
2243
|
let ext_list = ($extensions | split row ',' | each {|e| $e | str replace -r '^\\.' '' })
|
|
946
|
-
let all_items = (
|
|
2244
|
+
let all_items = (do $list_entries $ls_pattern)
|
|
947
2245
|
let dirs = $all_items | where type == dir
|
|
948
2246
|
let files = $all_items | where type == file | where {|f|
|
|
949
|
-
|
|
950
|
-
|
|
2247
|
+
$ext_list | any {|ext|
|
|
2248
|
+
$f.name | path basename | str ends-with ('.' + $ext)
|
|
2249
|
+
}
|
|
951
2250
|
}
|
|
952
2251
|
$dirs | append $files
|
|
953
2252
|
}
|
|
954
2253
|
},
|
|
955
2254
|
"directory" => {
|
|
956
|
-
(
|
|
2255
|
+
(do $list_entries $ls_pattern) | where type == dir
|
|
957
2256
|
},
|
|
958
2257
|
"any" => {
|
|
959
2258
|
if ($extensions | is-empty) {
|
|
960
|
-
|
|
2259
|
+
do $list_entries $ls_pattern
|
|
961
2260
|
} else {
|
|
962
2261
|
let ext_list = ($extensions | split row ',' | each {|e| $e | str replace -r '^\\.' '' })
|
|
963
|
-
let all_items = (
|
|
2262
|
+
let all_items = (do $list_entries $ls_pattern)
|
|
964
2263
|
let dirs = $all_items | where type == dir
|
|
965
2264
|
let files = $all_items | where type == file | where {|f|
|
|
966
|
-
|
|
967
|
-
|
|
2265
|
+
$ext_list | any {|ext|
|
|
2266
|
+
$f.name | path basename | str ends-with ('.' + $ext)
|
|
2267
|
+
}
|
|
968
2268
|
}
|
|
969
2269
|
$dirs | append $files
|
|
970
2270
|
}
|
|
@@ -979,7 +2279,7 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
|
|
|
979
2279
|
# even when the glob itself does not match directory names)
|
|
980
2280
|
let items = if ($glob_dir_pattern != null) and ($type != "directory") {
|
|
981
2281
|
let extra_dirs = try {
|
|
982
|
-
(
|
|
2282
|
+
(do $list_entries $glob_dir_pattern) | where type == dir
|
|
983
2283
|
} catch {
|
|
984
2284
|
[]
|
|
985
2285
|
}
|
|
@@ -1001,6 +2301,17 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
|
|
|
1001
2301
|
}
|
|
1002
2302
|
}
|
|
1003
2303
|
|
|
2304
|
+
let filtered = if $narrow_fixed_glob {
|
|
2305
|
+
$filtered | where {|item|
|
|
2306
|
+
# Any-mode listings already include directory symlinks; keep them
|
|
2307
|
+
# navigable without changing their existing completion rendering.
|
|
2308
|
+
let navigation_dir = ($item.type == dir) or ($item.type == symlink and (try { ($item.name | path expand | path type) == "dir" } catch { false }))
|
|
2309
|
+
let name = ($env.PWD | path join $item.name | path expand --no-symlink)
|
|
2310
|
+
let normalized_name = if $is_win { $name | str downcase } else { $name }
|
|
2311
|
+
$navigation_dir or ($normalized_name in $fixed_matches)
|
|
2312
|
+
}
|
|
2313
|
+
} else { $filtered }
|
|
2314
|
+
|
|
1004
2315
|
# Extract directory prefix to preserve in completion text
|
|
1005
2316
|
let dir_prefix = if ($glob_base | is-empty) {
|
|
1006
2317
|
""
|
|
@@ -1011,19 +2322,56 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
|
|
|
1011
2322
|
if ($parsed.parent | is-empty) { "" } else if ($parsed.parent | str ends-with "/") { $parsed.parent } else { $parsed.parent + "/" }
|
|
1012
2323
|
}
|
|
1013
2324
|
|
|
1014
|
-
# Format file completions
|
|
2325
|
+
# Format file completions, quoting names that Nushell would not read
|
|
2326
|
+
# back as a single argument
|
|
1015
2327
|
$filtered | each {|item|
|
|
1016
|
-
let
|
|
1017
|
-
|
|
2328
|
+
let base = if $item.type == dir {
|
|
2329
|
+
($item.name | path basename) + "/"
|
|
2330
|
+
} else {
|
|
2331
|
+
$item.name | path basename
|
|
2332
|
+
}
|
|
2333
|
+
let name = $dir_prefix + $base
|
|
2334
|
+
# Nushell expands a leading tilde even in a file name, but not after ./
|
|
2335
|
+
let spelled = if ($dir_prefix | is-empty) and ($base | str starts-with '~') {
|
|
2336
|
+
'./' + $base
|
|
2337
|
+
} else {
|
|
2338
|
+
$name
|
|
2339
|
+
}
|
|
2340
|
+
# A tilde or n-dot prefix typed by the user is meant to be expanded
|
|
2341
|
+
# by Nushell, which quoting would prevent, so expand such a prefix
|
|
2342
|
+
# here when the value has to be quoted
|
|
2343
|
+
let literal_ndots = ($base | str replace -r '[/\\\\]$' '') =~ '^\\.{3,}$'
|
|
2344
|
+
let expandable = ($dir_prefix | str starts-with '~') or ($dir_prefix | split row -r '[/\\\\]' | any {|s| $s =~ '^\\.{3,}$' })
|
|
2345
|
+
let value = if not ($literal_ndots or ($spelled | needs-quote --allow-expansion)) {
|
|
2346
|
+
$spelled
|
|
2347
|
+
} else if $expandable {
|
|
2348
|
+
let expanded_prefix = ($dir_prefix | path expand --no-symlink)
|
|
2349
|
+
let expanded_prefix = if ($expanded_prefix | str ends-with "/") { $expanded_prefix } else { $expanded_prefix + "/" }
|
|
2350
|
+
($expanded_prefix + $base) | quote-arg
|
|
2351
|
+
} else {
|
|
2352
|
+
$spelled | quote-arg
|
|
2353
|
+
}
|
|
2354
|
+
if ($insertion_prefix | is-not-empty) {
|
|
2355
|
+
# A key before the path keeps any tilde or n-dot segment from
|
|
2356
|
+
# being expanded, so quote the whole argument as written and show
|
|
2357
|
+
# the key in the menu as well.
|
|
2358
|
+
let keyed = $insertion_prefix + $name
|
|
2359
|
+
let keyed_value = ($keyed | quote-arg)
|
|
2360
|
+
if $keyed_value == $keyed {
|
|
2361
|
+
{ value: $keyed_value }
|
|
2362
|
+
} else {
|
|
2363
|
+
{ value: $keyed_value, display_override: $keyed }
|
|
2364
|
+
}
|
|
2365
|
+
} else if $value == $name {
|
|
2366
|
+
{ value: $value }
|
|
1018
2367
|
} else {
|
|
1019
|
-
$
|
|
2368
|
+
{ value: $value, display_override: $name }
|
|
1020
2369
|
}
|
|
1021
|
-
{ value: $name }
|
|
1022
2370
|
}
|
|
1023
2371
|
} else {
|
|
1024
2372
|
# Regular literal completion - split by tab
|
|
1025
2373
|
let parts = ($line | split row "\t")
|
|
1026
|
-
if ($parts | length) >= 2 {
|
|
2374
|
+
let record = if ($parts | length) >= 2 {
|
|
1027
2375
|
# value\\tdescription format
|
|
1028
2376
|
{ value: ($parts | get 0), description: ($parts | get 1) }
|
|
1029
2377
|
} else if ($parts | length) == 1 and ($parts | get 0 | is-not-empty) {
|
|
@@ -1032,6 +2380,18 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
|
|
|
1032
2380
|
} else {
|
|
1033
2381
|
null
|
|
1034
2382
|
}
|
|
2383
|
+
# Quote values that Nushell would not read back as a single argument,
|
|
2384
|
+
# while the menu keeps showing them as they are
|
|
2385
|
+
if $record == null {
|
|
2386
|
+
null
|
|
2387
|
+
} else {
|
|
2388
|
+
let value = ($record.value | quote-arg)
|
|
2389
|
+
if $value == $record.value {
|
|
2390
|
+
$record
|
|
2391
|
+
} else {
|
|
2392
|
+
$record | upsert value $value | insert display_override $record.value
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
1035
2395
|
}
|
|
1036
2396
|
} | flatten | compact
|
|
1037
2397
|
}
|
|
@@ -1069,13 +2429,13 @@ ${functionName}-external
|
|
|
1069
2429
|
if (i > 0) yield "\n";
|
|
1070
2430
|
if (suggestion.kind === "literal") {
|
|
1071
2431
|
const description = suggestion.description == null ? "" : sanitizeForTransport(formatMessage(suggestion.description, { colors: false }));
|
|
1072
|
-
yield `${
|
|
2432
|
+
yield `${encodeLiteral(suggestion.text)}\t${description}`;
|
|
1073
2433
|
} else {
|
|
1074
2434
|
const extensions = encodeExtensions(suggestion.extensions);
|
|
1075
2435
|
const hidden = suggestion.includeHidden ? "1" : "0";
|
|
1076
2436
|
const description = suggestion.description == null ? "" : sanitizeForTransport(formatMessage(suggestion.description, { colors: false }));
|
|
1077
2437
|
const pattern = encodePattern(suggestion.pattern ?? "");
|
|
1078
|
-
yield `__FILE__:${suggestion.type}:${extensions}:${pattern}:${hidden}\t${description}`;
|
|
2438
|
+
yield `__FILE__:${suggestion.type}:${extensions}:${pattern}:${hidden}${encodeFilePrefix(suggestion)}\t${description}`;
|
|
1079
2439
|
}
|
|
1080
2440
|
i++;
|
|
1081
2441
|
}
|
|
@@ -1094,27 +2454,48 @@ const pwsh = {
|
|
|
1094
2454
|
Register-ArgumentCompleter -Native -CommandName ${programName} -ScriptBlock {
|
|
1095
2455
|
param(\$wordToComplete, \$commandAst, \$cursorPosition)
|
|
1096
2456
|
|
|
2457
|
+
# CompletionText is shell syntax; display labels remain unquoted.
|
|
2458
|
+
\$quoteArgument = {
|
|
2459
|
+
param([string]\$value)
|
|
2460
|
+
if (\$value -cmatch '\\A[a-zA-Z0-9_./:\\\\-]+\\z' -and \$value -cnotmatch '\\A-(?!-).*[.:]' -and \$value -cnotmatch '\\A-?(?:[0-9]|\\.[0-9])') {
|
|
2461
|
+
return \$value
|
|
2462
|
+
}
|
|
2463
|
+
return "'" + [System.Management.Automation.Language.CodeGeneration]::EscapeSingleQuotedStringContent(\$value) + "'"
|
|
2464
|
+
}
|
|
2465
|
+
|
|
1097
2466
|
# Extract arguments from AST (handles quoted strings properly)
|
|
1098
2467
|
\$arguments = @()
|
|
1099
2468
|
\$commandElements = \$commandAst.CommandElements
|
|
2469
|
+
\$valuePrefix = \$wordToComplete
|
|
2470
|
+
\$quoteType = \$null
|
|
1100
2471
|
|
|
1101
|
-
#
|
|
1102
|
-
#
|
|
1103
|
-
\$
|
|
1104
|
-
if (\$commandElements.Count -gt 1) {
|
|
1105
|
-
\$lastElement = \$commandElements[\$commandElements.Count - 1]
|
|
1106
|
-
\$lastText = if (\$lastElement -is [System.Management.Automation.Language.StringConstantExpressionAst]) {
|
|
1107
|
-
\$lastElement.Value
|
|
1108
|
-
} else {
|
|
1109
|
-
\$lastElement.Extent.Text
|
|
1110
|
-
}
|
|
1111
|
-
if (\$lastText -eq \$wordToComplete) {
|
|
1112
|
-
\$maxIndex = \$commandElements.Count - 2
|
|
1113
|
-
}
|
|
1114
|
-
}
|
|
1115
|
-
|
|
1116
|
-
for (\$i = 1; \$i -le \$maxIndex; \$i++) {
|
|
2472
|
+
# Keep only complete arguments before the cursor. The current word
|
|
2473
|
+
# (including a token ending exactly at the cursor) is appended below.
|
|
2474
|
+
for (\$i = 1; \$i -lt \$commandElements.Count; \$i++) {
|
|
1117
2475
|
\$element = \$commandElements[\$i]
|
|
2476
|
+
if (\$element.Extent.EndOffset -ge \$cursorPosition) {
|
|
2477
|
+
# wordToComplete has synthesized delimiters and decoded escapes.
|
|
2478
|
+
# Only the AST retains the literal value and original quote type.
|
|
2479
|
+
# PSReadLine leaves the cursor before a directory's closing quote.
|
|
2480
|
+
if (\$element -is [System.Management.Automation.Language.StringConstantExpressionAst] -and
|
|
2481
|
+
(\$element.Extent.EndOffset -eq \$cursorPosition -or
|
|
2482
|
+
(\$element.Extent.EndOffset -eq (\$cursorPosition + 1) -and
|
|
2483
|
+
\$element.StringConstantType -in @('SingleQuoted', 'DoubleQuoted')))) {
|
|
2484
|
+
if (\$element.StringConstantType -in @('SingleQuoted', 'DoubleQuoted')) {
|
|
2485
|
+
\$valuePrefix = \$element.Value
|
|
2486
|
+
\$quoteType = \$element.StringConstantType
|
|
2487
|
+
} elseif (\$element.Extent.Text -match "^[^=]+=['\\u2018-\\u201b]") {
|
|
2488
|
+
# Joined values are BareWord constants despite their quotes.
|
|
2489
|
+
# Quote the entire replacement to keep it one argument.
|
|
2490
|
+
\$valuePrefix = \$element.Value
|
|
2491
|
+
\$quoteType = 'SingleQuoted'
|
|
2492
|
+
} elseif (\$element.Extent.Text -match '^[^=]+=["\\u201c-\\u201e]') {
|
|
2493
|
+
\$valuePrefix = \$element.Value
|
|
2494
|
+
\$quoteType = 'DoubleQuoted'
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
break
|
|
2498
|
+
}
|
|
1118
2499
|
|
|
1119
2500
|
if (\$element -is [System.Management.Automation.Language.StringConstantExpressionAst]) {
|
|
1120
2501
|
\$arguments += \$element.Value
|
|
@@ -1123,30 +2504,54 @@ Register-ArgumentCompleter -Native -CommandName ${programName} -ScriptBlock {
|
|
|
1123
2504
|
}
|
|
1124
2505
|
}
|
|
1125
2506
|
|
|
2507
|
+
# CompletionText replaces the whole token, including its opening quote.
|
|
2508
|
+
# Escape literals without evaluating variables or expressions.
|
|
2509
|
+
\$quoteCompletion = {
|
|
2510
|
+
param(\$text)
|
|
2511
|
+
if (\$quoteType -eq 'SingleQuoted') {
|
|
2512
|
+
"'" + (\$text -replace "['\\u2018-\\u201b]", '\$0\$0') + "'"
|
|
2513
|
+
} elseif (\$quoteType -eq 'DoubleQuoted') {
|
|
2514
|
+
'"' + (\$text -replace '[\\x60\$"\\u201c-\\u201e]', '\`\$0') + '"'
|
|
2515
|
+
} else {
|
|
2516
|
+
& \$quoteArgument \$text
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
|
|
1126
2520
|
# Build arguments array for completion command
|
|
1127
2521
|
\$completionArgs = @()
|
|
1128
2522
|
${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
|
|
1129
2523
|
` : ""} \$completionArgs += \$arguments
|
|
1130
|
-
\$completionArgs += \$wordToComplete
|
|
1131
2524
|
|
|
1132
2525
|
# Call completion command and capture output
|
|
1133
2526
|
try {
|
|
1134
|
-
|
|
2527
|
+
# Quote the prefix separately: array arguments expand a literal tilde
|
|
2528
|
+
# when passed to native programs on Unix.
|
|
2529
|
+
\$output = & ${programName} \$completionArgs "\$valuePrefix" 2>\$null
|
|
1135
2530
|
if (-not \$output) { return }
|
|
1136
2531
|
|
|
1137
2532
|
# Parse tab-separated output and create CompletionResult objects
|
|
1138
2533
|
\$output -split "\`n" | ForEach-Object {
|
|
1139
|
-
\$
|
|
2534
|
+
\$literal = \$_.StartsWith('__LITERAL__:', [System.StringComparison]::Ordinal)
|
|
2535
|
+
# Quoted values preserve whitespace; only remove the CR of CRLF.
|
|
2536
|
+
# Keep the existing trimming for unquoted completions.
|
|
2537
|
+
\$line = if (\$null -ne \$quoteType) { \$_.TrimEnd([char]13) } else { \$_.Trim() }
|
|
1140
2538
|
if (-not \$line) { return }
|
|
2539
|
+
if (\$literal) { \$line = \$line.Substring(12) }
|
|
1141
2540
|
|
|
1142
|
-
if (\$
|
|
2541
|
+
if (-not \$literal -and \$_.StartsWith('__FILE__:', [System.StringComparison]::Ordinal)) {
|
|
1143
2542
|
# Parse file completion directive: __FILE__:type:extensions:pattern:hidden
|
|
1144
2543
|
\$directive = (\$line -split "\`t")[0]
|
|
1145
|
-
\$parts = \$directive -split ':',
|
|
2544
|
+
\$parts = \$directive -split ':', 6
|
|
1146
2545
|
\$type = \$parts[1]
|
|
1147
2546
|
\$extensions = \$parts[2]
|
|
1148
2547
|
\$pattern = \$parts[3] -replace '%3A', ':' -replace '%25', '%'
|
|
1149
2548
|
\$hidden = \$parts[4] -eq '1'
|
|
2549
|
+
\$insertionPrefix = if (\$parts.Length -gt 5) { \$parts[5] -replace '%3A', ':' -replace '%25', '%' } else { '' }
|
|
2550
|
+
\$matchingWord = \$valuePrefix
|
|
2551
|
+
if (\$insertionPrefix -and \$pattern.StartsWith(\$insertionPrefix, [System.StringComparison]::Ordinal) -and \$matchingWord.StartsWith(\$insertionPrefix, [System.StringComparison]::Ordinal)) {
|
|
2552
|
+
\$pattern = \$pattern.Substring(\$insertionPrefix.Length)
|
|
2553
|
+
\$matchingWord = \$matchingWord.Substring(\$insertionPrefix.Length)
|
|
2554
|
+
} else { \$insertionPrefix = '' }
|
|
1150
2555
|
|
|
1151
2556
|
# When a pattern is specified, use it as the file matching
|
|
1152
2557
|
# prefix instead of the current word. If the user has
|
|
@@ -1155,7 +2560,7 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
|
|
|
1155
2560
|
# comparing so that Windows backslashes match forward slashes
|
|
1156
2561
|
# in the transported pattern.
|
|
1157
2562
|
\$normalizedPattern = if (\$pattern) { \$pattern.Replace('\\', '/') -replace '^\\./','' } else { '' }
|
|
1158
|
-
\$normalizedWord = if (\$
|
|
2563
|
+
\$normalizedWord = if (\$matchingWord) { \$matchingWord.Replace('\\', '/') -replace '^\\./','' } else { '' }
|
|
1159
2564
|
\$comparison = if (\$IsWindows) { [System.StringComparison]::OrdinalIgnoreCase } else { [System.StringComparison]::Ordinal }
|
|
1160
2565
|
# For wildcard patterns, compare only the directory prefix
|
|
1161
2566
|
\$comparePattern = \$normalizedPattern
|
|
@@ -1167,12 +2572,25 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
|
|
|
1167
2572
|
\$comparePattern = ''
|
|
1168
2573
|
}
|
|
1169
2574
|
}
|
|
1170
|
-
|
|
1171
|
-
|
|
2575
|
+
# A quoted home-pattern directory inserts an absolute path.
|
|
2576
|
+
# Compare subsequent input against the same expanded prefix.
|
|
2577
|
+
# Detect home syntax before removing ./, which can target a
|
|
2578
|
+
# literal local tilde directory rather than the home directory.
|
|
2579
|
+
\$homePattern = \$pattern -and (\$pattern -eq '~' -or \$pattern.Replace('\\', '/').StartsWith('~/'))
|
|
2580
|
+
if (\$homePattern) {
|
|
2581
|
+
# Preserve the home root when the pattern is exactly ~.
|
|
2582
|
+
\$expandedPattern = if (\$comparePattern -eq '~') { \$HOME } else { \$HOME.Replace('\\', '/').TrimEnd('/') + \$comparePattern.Substring(1) }
|
|
2583
|
+
\$expandedPattern = [IO.Path]::GetFullPath(\$expandedPattern).Replace('\\', '/')
|
|
2584
|
+
if (\$normalizedWord.StartsWith(\$expandedPattern, \$comparison)) {
|
|
2585
|
+
\$comparePattern = \$expandedPattern
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
\$prefix = if ((\$comparePattern -or \$normalizedWord) -and \$normalizedWord -and \$normalizedWord.StartsWith(\$comparePattern, \$comparison) -and \$normalizedWord.Length -ge \$comparePattern.Length -and \$matchingWord -ne \$pattern) {
|
|
2589
|
+
\$matchingWord
|
|
1172
2590
|
} elseif (\$pattern) {
|
|
1173
2591
|
\$pattern
|
|
1174
|
-
} elseif (\$
|
|
1175
|
-
\$
|
|
2592
|
+
} elseif (\$matchingWord) {
|
|
2593
|
+
\$matchingWord
|
|
1176
2594
|
} else { '' }
|
|
1177
2595
|
|
|
1178
2596
|
# Use -Force to include hidden files when requested, or when
|
|
@@ -1180,15 +2598,35 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
|
|
|
1180
2598
|
\$prefixBasename = if (\$prefix) { Split-Path -Leaf \$prefix } else { '' }
|
|
1181
2599
|
\$forceParam = if (\$hidden -or (\$prefixBasename -and \$prefixBasename.StartsWith('.'))) { @{Force = \$true} } else { @{} }
|
|
1182
2600
|
|
|
2601
|
+
# A quoted tilde is literal when passed to a native command.
|
|
2602
|
+
# Prevent PowerShell's file provider from expanding it to home.
|
|
2603
|
+
# Parser-provided home patterns still need home expansion.
|
|
2604
|
+
\$prefixFromInput = \$matchingWord -and \$prefix -ceq \$matchingWord
|
|
2605
|
+
\$lookupPrefix = if (\$prefixFromInput -and \$null -ne \$quoteType -and \$prefix.StartsWith('~')) { './' + \$prefix } else { \$prefix }
|
|
2606
|
+
|
|
2607
|
+
# Detect narrowing before directory normalization changes prefix.
|
|
2608
|
+
\$narrowFixedGlob = \$type -ne 'directory' -and \$pattern -match '[\*\?]' -and \$prefix -ceq \$matchingWord -and \$pattern -cne \$matchingWord
|
|
2609
|
+
|
|
1183
2610
|
# If prefix is a directory without trailing slash, append
|
|
1184
2611
|
# one so Get-ChildItem lists its contents
|
|
1185
|
-
if (\$prefix -and (Test-Path -Path \$
|
|
2612
|
+
if (\$prefix -and (Test-Path -Path \$lookupPrefix -PathType Container) -and -not \$prefix.EndsWith('/') -and -not \$prefix.EndsWith('\\')) {
|
|
1186
2613
|
\$prefix = \$prefix + '/'
|
|
2614
|
+
\$lookupPrefix = \$lookupPrefix + '/'
|
|
1187
2615
|
}
|
|
1188
2616
|
|
|
1189
2617
|
# Build the glob path—when the prefix contains wildcard
|
|
1190
2618
|
# characters, use it as-is; otherwise append *
|
|
1191
|
-
\$globPath = if (\$
|
|
2619
|
+
\$globPath = if (\$lookupPrefix -match '[\*\?]') { \$lookupPrefix } else { "\${lookupPrefix}*" }
|
|
2620
|
+
|
|
2621
|
+
# Keep the original native glob as an independent file filter
|
|
2622
|
+
# when the current word narrows its candidates.
|
|
2623
|
+
\$fixedMatches = @()
|
|
2624
|
+
if (\$narrowFixedGlob) {
|
|
2625
|
+
# Include hidden parents here; candidate enumeration still
|
|
2626
|
+
# controls which hidden children can be suggested.
|
|
2627
|
+
\$fixedMatches = @(Get-Item -Force -Path \$pattern -ErrorAction SilentlyContinue |
|
|
2628
|
+
Select-Object -ExpandProperty FullName)
|
|
2629
|
+
}
|
|
1192
2630
|
|
|
1193
2631
|
# Get file system items based on type
|
|
1194
2632
|
\$items = @()
|
|
@@ -1199,7 +2637,10 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
|
|
|
1199
2637
|
\$extList = (\$extensions -split ',') -replace '^\\.',''
|
|
1200
2638
|
\$items = Get-ChildItem @forceParam -Path \$globPath -ErrorAction SilentlyContinue |
|
|
1201
2639
|
Where-Object {
|
|
1202
|
-
\$
|
|
2640
|
+
\$fileName = \$_.Name
|
|
2641
|
+
\$_.PSIsContainer -or (@(\$extList | Where-Object {
|
|
2642
|
+
\$fileName.EndsWith('.' + \$_, [System.StringComparison]::Ordinal)
|
|
2643
|
+
}).Count -gt 0)
|
|
1203
2644
|
}
|
|
1204
2645
|
} else {
|
|
1205
2646
|
\$items = Get-ChildItem @forceParam -Path \$globPath -ErrorAction SilentlyContinue
|
|
@@ -1214,7 +2655,10 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
|
|
|
1214
2655
|
\$extList = (\$extensions -split ',') -replace '^\\.',''
|
|
1215
2656
|
\$items = Get-ChildItem @forceParam -Path \$globPath -ErrorAction SilentlyContinue |
|
|
1216
2657
|
Where-Object {
|
|
1217
|
-
\$
|
|
2658
|
+
\$fileName = \$_.Name
|
|
2659
|
+
\$_.PSIsContainer -or (@(\$extList | Where-Object {
|
|
2660
|
+
\$fileName.EndsWith('.' + \$_, [System.StringComparison]::Ordinal)
|
|
2661
|
+
}).Count -gt 0)
|
|
1218
2662
|
}
|
|
1219
2663
|
} else {
|
|
1220
2664
|
\$items = Get-ChildItem @forceParam -Path \$globPath -ErrorAction SilentlyContinue
|
|
@@ -1225,7 +2669,7 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
|
|
|
1225
2669
|
# For file/any modes with glob patterns, also add directories
|
|
1226
2670
|
# from the base directory for navigation
|
|
1227
2671
|
if (\$prefix -match '[\*\?]' -and \$type -ne 'directory') {
|
|
1228
|
-
\$globDir = Split-Path -Parent \$
|
|
2672
|
+
\$globDir = Split-Path -Parent \$lookupPrefix
|
|
1229
2673
|
if (-not \$globDir) { \$globDir = '.' }
|
|
1230
2674
|
\$extraDirs = Get-ChildItem @forceParam -Directory -Path "\$globDir/*" -ErrorAction SilentlyContinue
|
|
1231
2675
|
if (\$extraDirs) { \$items = @(\$items) + @(\$extraDirs) | Select-Object -Unique }
|
|
@@ -1236,6 +2680,14 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
|
|
|
1236
2680
|
\$items = \$items | Where-Object { -not \$_.Attributes.HasFlag([System.IO.FileAttributes]::Hidden) }
|
|
1237
2681
|
}
|
|
1238
2682
|
|
|
2683
|
+
if (\$narrowFixedGlob) {
|
|
2684
|
+
\$items = \$items | Where-Object {
|
|
2685
|
+
if (\$_.PSIsContainer) { \$true }
|
|
2686
|
+
elseif (\$IsWindows) { \$fixedMatches -contains \$_.FullName }
|
|
2687
|
+
else { \$fixedMatches -ccontains \$_.FullName }
|
|
2688
|
+
}
|
|
2689
|
+
}
|
|
2690
|
+
|
|
1239
2691
|
# Extract directory prefix to preserve in completion text
|
|
1240
2692
|
\$dirPrefix = if (\$prefix -and (\$prefix.Contains('/') -or \$prefix.Contains('\\'))) {
|
|
1241
2693
|
\$slashIdx = [Math]::Max(\$prefix.LastIndexOf('/'), \$prefix.LastIndexOf('\\'))
|
|
@@ -1246,10 +2698,34 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
|
|
|
1246
2698
|
\$items | ForEach-Object {
|
|
1247
2699
|
\$completionText = if (\$_.PSIsContainer) { "\$dirPrefix\$(\$_.Name)/" } else { "\$dirPrefix\$(\$_.Name)" }
|
|
1248
2700
|
\$itemType = if (\$_.PSIsContainer) { 'Directory' } else { 'File' }
|
|
2701
|
+
\$resultType = if (\$_.PSIsContainer) { 'ProviderContainer' } else { 'ParameterValue' }
|
|
2702
|
+
# PSReadLine recognizes the platform separator for containers.
|
|
2703
|
+
# Keep the display label's existing slash convention.
|
|
2704
|
+
\$directorySuffix = [IO.Path]::DirectorySeparatorChar
|
|
2705
|
+
\$pathText = if (\$_.PSIsContainer) { "\$dirPrefix\$(\$_.Name)\$directorySuffix" } else { \$completionText }
|
|
2706
|
+
# Quoting disables native tilde expansion. A home pattern
|
|
2707
|
+
# supplied by the parser must insert the discovered path.
|
|
2708
|
+
\$replacementText = if (\$null -ne \$quoteType -and -not \$prefixFromInput -and \$homePattern) {
|
|
2709
|
+
if (\$_.PSIsContainer) { \$_.FullName + \$directorySuffix } else { \$_.FullName }
|
|
2710
|
+
} else { \$pathText }
|
|
2711
|
+
# Quoting suppresses native tilde expansion. Keep safe
|
|
2712
|
+
# ~/ paths bare; otherwise use the enumerated full path.
|
|
2713
|
+
# Keyed paths keep the typed spelling, including ~/, and
|
|
2714
|
+
# always take the quoted argument form.
|
|
2715
|
+
\$insertionText = if (\$null -ne \$quoteType) {
|
|
2716
|
+
& \$quoteCompletion (\$insertionPrefix + \$replacementText)
|
|
2717
|
+
} elseif (-not \$insertionPrefix -and \$completionText -cmatch '\\A~/[a-zA-Z0-9_./:\\\\-]*\\z') {
|
|
2718
|
+
\$pathText
|
|
2719
|
+
} else {
|
|
2720
|
+
\$argumentText = if (-not \$insertionPrefix -and (\$completionText.StartsWith('~/') -or \$completionText.StartsWith('~\\'))) {
|
|
2721
|
+
if (\$_.PSIsContainer) { \$_.FullName + \$directorySuffix } else { \$_.FullName }
|
|
2722
|
+
} else { \$pathText }
|
|
2723
|
+
& \$quoteArgument (\$insertionPrefix + \$argumentText)
|
|
2724
|
+
}
|
|
1249
2725
|
[System.Management.Automation.CompletionResult]::new(
|
|
2726
|
+
\$insertionText,
|
|
1250
2727
|
\$completionText,
|
|
1251
|
-
\$
|
|
1252
|
-
'ParameterValue',
|
|
2728
|
+
\$resultType,
|
|
1253
2729
|
\$itemType
|
|
1254
2730
|
)
|
|
1255
2731
|
}
|
|
@@ -1257,11 +2733,14 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
|
|
|
1257
2733
|
# Parse literal completion: text\\tlistItemText\\tdescription
|
|
1258
2734
|
\$parts = \$line -split "\`t", 3
|
|
1259
2735
|
\$completionText = \$parts[0]
|
|
2736
|
+
# Empty display text is invalid for CompletionResult. Keep
|
|
2737
|
+
# ignoring empty suggestions without dropping later records.
|
|
2738
|
+
if (-not \$completionText) { return }
|
|
1260
2739
|
\$listItemText = if (\$parts.Length -gt 1 -and \$parts[1]) { \$parts[1] } else { \$completionText }
|
|
1261
2740
|
\$toolTip = if (\$parts.Length -gt 2 -and \$parts[2]) { \$parts[2] } else { \$completionText }
|
|
1262
2741
|
|
|
1263
2742
|
[System.Management.Automation.CompletionResult]::new(
|
|
1264
|
-
\$completionText,
|
|
2743
|
+
(& \$quoteCompletion \$completionText),
|
|
1265
2744
|
\$listItemText,
|
|
1266
2745
|
'ParameterValue',
|
|
1267
2746
|
\$toolTip
|
|
@@ -1281,13 +2760,13 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
|
|
|
1281
2760
|
if (suggestion.kind === "literal") {
|
|
1282
2761
|
const description = suggestion.description == null ? "" : sanitizeForTransport(formatMessage(suggestion.description, { colors: false }));
|
|
1283
2762
|
const text = sanitizeForTransport(suggestion.text);
|
|
1284
|
-
yield `${text}\t${text}\t${description}`;
|
|
2763
|
+
yield `${encodeLiteral(suggestion.text)}\t${text}\t${description}`;
|
|
1285
2764
|
} else {
|
|
1286
2765
|
const extensions = encodeExtensions(suggestion.extensions);
|
|
1287
2766
|
const hidden = suggestion.includeHidden ? "1" : "0";
|
|
1288
2767
|
const description = suggestion.description == null ? "" : sanitizeForTransport(formatMessage(suggestion.description, { colors: false }));
|
|
1289
2768
|
const pattern = encodePattern(suggestion.pattern ?? "");
|
|
1290
|
-
yield `__FILE__:${suggestion.type}:${extensions}:${pattern}:${hidden}\t[file]\t${description}`;
|
|
2769
|
+
yield `__FILE__:${suggestion.type}:${extensions}:${pattern}:${hidden}${encodeFilePrefix(suggestion)}\t[file]\t${description}`;
|
|
1291
2770
|
}
|
|
1292
2771
|
i++;
|
|
1293
2772
|
}
|