@optique/core 1.0.0-dev.908 → 1.0.0
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/annotation-state.cjs +425 -0
- package/dist/annotation-state.d.cts +24 -0
- package/dist/annotation-state.d.ts +24 -0
- package/dist/annotation-state.js +414 -0
- package/dist/annotations.cjs +2 -248
- package/dist/annotations.d.cts +2 -137
- package/dist/annotations.d.ts +2 -137
- package/dist/annotations.js +2 -238
- package/dist/completion.cjs +611 -100
- package/dist/completion.d.cts +1 -1
- package/dist/completion.d.ts +1 -1
- package/dist/completion.js +611 -100
- package/dist/constructs.cjs +3338 -827
- package/dist/constructs.d.cts +48 -7
- package/dist/constructs.d.ts +48 -7
- package/dist/constructs.js +3338 -827
- package/dist/context.cjs +0 -23
- package/dist/context.d.cts +119 -53
- package/dist/context.d.ts +119 -53
- package/dist/context.js +0 -22
- package/dist/dependency-metadata.cjs +139 -0
- package/dist/dependency-metadata.d.cts +112 -0
- package/dist/dependency-metadata.d.ts +112 -0
- package/dist/dependency-metadata.js +138 -0
- package/dist/dependency-runtime.cjs +698 -0
- package/dist/dependency-runtime.d.cts +149 -0
- package/dist/dependency-runtime.d.ts +149 -0
- package/dist/dependency-runtime.js +687 -0
- package/dist/dependency.cjs +7 -928
- package/dist/dependency.d.cts +2 -794
- package/dist/dependency.d.ts +2 -794
- package/dist/dependency.js +2 -899
- package/dist/displaywidth.cjs +44 -0
- package/dist/displaywidth.js +43 -0
- package/dist/doc.cjs +285 -23
- package/dist/doc.d.cts +57 -2
- package/dist/doc.d.ts +57 -2
- package/dist/doc.js +283 -25
- package/dist/execution-context.cjs +56 -0
- package/dist/execution-context.js +53 -0
- package/dist/extension.cjs +87 -0
- package/dist/extension.d.cts +97 -0
- package/dist/extension.d.ts +97 -0
- package/dist/extension.js +76 -0
- package/dist/facade.cjs +718 -523
- package/dist/facade.d.cts +87 -18
- package/dist/facade.d.ts +87 -18
- package/dist/facade.js +718 -523
- package/dist/index.cjs +14 -29
- package/dist/index.d.cts +10 -10
- package/dist/index.d.ts +10 -10
- package/dist/index.js +7 -7
- package/dist/input-trace.cjs +56 -0
- package/dist/input-trace.d.cts +77 -0
- package/dist/input-trace.d.ts +77 -0
- package/dist/input-trace.js +55 -0
- package/dist/internal/annotations.cjs +316 -0
- package/dist/internal/annotations.d.cts +140 -0
- package/dist/internal/annotations.d.ts +140 -0
- package/dist/internal/annotations.js +306 -0
- package/dist/internal/dependency.cjs +984 -0
- package/dist/internal/dependency.d.cts +539 -0
- package/dist/internal/dependency.d.ts +539 -0
- package/dist/internal/dependency.js +964 -0
- package/dist/{mode-dispatch.cjs → internal/mode-dispatch.cjs} +1 -3
- package/dist/{mode-dispatch.d.cts → internal/mode-dispatch.d.cts} +3 -7
- package/dist/{mode-dispatch.d.ts → internal/mode-dispatch.d.ts} +3 -7
- package/dist/{mode-dispatch.js → internal/mode-dispatch.js} +1 -3
- package/dist/internal/parser.cjs +728 -0
- package/dist/internal/parser.d.cts +947 -0
- package/dist/internal/parser.d.ts +947 -0
- package/dist/internal/parser.js +711 -0
- package/dist/message.cjs +84 -26
- package/dist/message.d.cts +49 -9
- package/dist/message.d.ts +49 -9
- package/dist/message.js +84 -27
- package/dist/modifiers.cjs +1023 -240
- package/dist/modifiers.d.cts +42 -1
- package/dist/modifiers.d.ts +42 -1
- package/dist/modifiers.js +1023 -240
- package/dist/parser.cjs +11 -463
- package/dist/parser.d.cts +3 -537
- package/dist/parser.d.ts +3 -537
- package/dist/parser.js +2 -433
- package/dist/phase2-seed.cjs +59 -0
- package/dist/phase2-seed.js +56 -0
- package/dist/primitives.cjs +557 -208
- package/dist/primitives.d.cts +10 -14
- package/dist/primitives.d.ts +10 -14
- package/dist/primitives.js +557 -208
- package/dist/program.cjs +5 -1
- package/dist/program.d.cts +5 -3
- package/dist/program.d.ts +5 -3
- package/dist/program.js +6 -1
- package/dist/suggestion.cjs +22 -8
- package/dist/suggestion.js +22 -8
- package/dist/usage-internals.cjs +3 -2
- package/dist/usage-internals.js +4 -2
- package/dist/usage.cjs +195 -40
- package/dist/usage.d.cts +92 -11
- package/dist/usage.d.ts +92 -11
- package/dist/usage.js +194 -41
- package/dist/validate.cjs +170 -0
- package/dist/validate.js +164 -0
- package/dist/valueparser.cjs +1278 -191
- package/dist/valueparser.d.cts +330 -20
- package/dist/valueparser.d.ts +330 -20
- package/dist/valueparser.js +1277 -192
- package/package.json +9 -9
package/dist/completion.js
CHANGED
|
@@ -31,6 +31,31 @@ function validateProgramName(programName) {
|
|
|
31
31
|
if (!SAFE_PROGRAM_NAME_PATTERN.test(programName)) throw new Error(`Invalid program name for shell completion: "${programName}". Program names must start with an alphanumeric character or underscore, and contain only alphanumeric characters, underscores, hyphens, and dots.`);
|
|
32
32
|
}
|
|
33
33
|
/**
|
|
34
|
+
* Percent-encodes colons and percent signs in a pattern string so that it
|
|
35
|
+
* can be safely embedded in the colon-delimited `__FILE__` transport format.
|
|
36
|
+
* @param pattern The raw pattern string.
|
|
37
|
+
* @returns The encoded pattern string.
|
|
38
|
+
* @internal
|
|
39
|
+
*/
|
|
40
|
+
function encodePattern(pattern) {
|
|
41
|
+
return pattern.replace(/%/g, "%25").replace(/:/g, "%3A");
|
|
42
|
+
}
|
|
43
|
+
function encodeExtensions(extensions) {
|
|
44
|
+
return extensions?.map((ext) => ext.replace(/^\./, "")).join(",") ?? "";
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Replaces control characters that would corrupt shell completion protocols.
|
|
48
|
+
* Shell completion formats use tabs as field delimiters and newlines as record
|
|
49
|
+
* delimiters. Null bytes are used as delimiters in zsh's format.
|
|
50
|
+
* This is used for both suggestion text and descriptions.
|
|
51
|
+
* @param text The string to sanitize.
|
|
52
|
+
* @returns The sanitized string with control characters replaced by spaces.
|
|
53
|
+
* @internal
|
|
54
|
+
*/
|
|
55
|
+
function sanitizeForTransport(text) {
|
|
56
|
+
return text.replace(/[\t\n\r\0]/g, " ");
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
34
59
|
* The Bash shell completion generator.
|
|
35
60
|
* @since 0.6.0
|
|
36
61
|
*/
|
|
@@ -48,29 +73,153 @@ function _${programName} () {
|
|
|
48
73
|
if [[ "$line" == __FILE__:* ]]; then
|
|
49
74
|
# Parse file completion directive: __FILE__:type:extensions:pattern:hidden
|
|
50
75
|
IFS=':' read -r _ type extensions pattern hidden <<< "$line"
|
|
76
|
+
extensions="\${extensions//,\\./,}"; extensions="\${extensions#.}"
|
|
77
|
+
pattern="\${pattern//%3A/:}"; pattern="\${pattern//%25/%}"
|
|
78
|
+
|
|
79
|
+
# Save and adjust glob/shell options for safe file completion
|
|
80
|
+
local __dotglob_was_set=0 __failglob_was_set=0 __noglob_was_set=0
|
|
81
|
+
local __globignore_was_set=0 __saved_globignore="\${GLOBIGNORE-}"
|
|
82
|
+
[[ \${GLOBIGNORE+x} == x ]] && __globignore_was_set=1
|
|
83
|
+
shopt -q dotglob && __dotglob_was_set=1
|
|
84
|
+
shopt -q failglob && __failglob_was_set=1
|
|
85
|
+
[[ $- == *f* ]] && __noglob_was_set=1
|
|
86
|
+
# Unset GLOBIGNORE before enabling dotglob because unsetting
|
|
87
|
+
# GLOBIGNORE implicitly clears dotglob in Bash
|
|
88
|
+
shopt -u failglob 2>/dev/null
|
|
89
|
+
set +f
|
|
90
|
+
unset GLOBIGNORE
|
|
91
|
+
|
|
92
|
+
# Expand tilde prefix for file globbing
|
|
93
|
+
local __glob_current="$current" __tilde_prefix="" __tilde_expanded=""
|
|
94
|
+
if [[ "$current" =~ ^(~[a-zA-Z0-9_.+-]*)(/.*)$ ]]; then
|
|
95
|
+
__tilde_prefix="\${BASH_REMATCH[1]}"
|
|
96
|
+
eval "__tilde_expanded=\$__tilde_prefix" 2>/dev/null || true
|
|
97
|
+
if [[ -n "$__tilde_expanded" && "$__tilde_expanded" != "$__tilde_prefix" ]]; then
|
|
98
|
+
__glob_current="\${__tilde_expanded}\${current#\$__tilde_prefix}"
|
|
99
|
+
else
|
|
100
|
+
__tilde_prefix=""
|
|
101
|
+
fi
|
|
102
|
+
fi
|
|
103
|
+
|
|
104
|
+
# When a pattern is specified, use it as the glob base instead of the
|
|
105
|
+
# current word so that completions enumerate the pattern's directory.
|
|
106
|
+
# However, if the user has already typed beyond the pattern (e.g.,
|
|
107
|
+
# pattern="src/" and current="src/ma"), preserve the typed suffix
|
|
108
|
+
# for incremental filtering.
|
|
109
|
+
local __from_pattern=0
|
|
110
|
+
if [[ -n "$pattern" ]]; then
|
|
111
|
+
# Normalize leading ./ so that ./src/ma matches pattern src/
|
|
112
|
+
local __norm_current="\${current#./}"
|
|
113
|
+
local __norm_pattern="\${pattern#./}"
|
|
114
|
+
# For wildcard patterns, compare only the directory prefix before
|
|
115
|
+
# any wildcards so that typing src/ma narrows src/*.ts correctly
|
|
116
|
+
local __compare_pattern="$__norm_pattern"
|
|
117
|
+
if [[ "$__compare_pattern" == *[\*\?]* ]]; then
|
|
118
|
+
__compare_pattern="\${__compare_pattern%%[\*\?]*}"
|
|
119
|
+
[[ "$__compare_pattern" == */* ]] && __compare_pattern="\${__compare_pattern%/*}/" || __compare_pattern=""
|
|
120
|
+
fi
|
|
121
|
+
if [[ ( -n "$__compare_pattern" || -n "$__norm_current" ) && \${#__norm_current} -ge \${#__compare_pattern} && "\${__norm_current:0:\${#__compare_pattern}}" == "$__compare_pattern" && "$current" != "$pattern" ]]; then
|
|
122
|
+
# User has typed beyond or an equivalent form of the pattern
|
|
123
|
+
true
|
|
124
|
+
else
|
|
125
|
+
__from_pattern=1
|
|
126
|
+
# Reset tilde state from the current-word expansion so that a
|
|
127
|
+
# non-tilde pattern is not rewritten through stale tilde state
|
|
128
|
+
__tilde_prefix=""
|
|
129
|
+
__tilde_expanded=""
|
|
130
|
+
__glob_current="$pattern"
|
|
131
|
+
if [[ "$pattern" =~ ^(~[a-zA-Z0-9_.+-]*)(/.*)?$ ]]; then
|
|
132
|
+
__tilde_prefix="\${BASH_REMATCH[1]}"
|
|
133
|
+
eval "__tilde_expanded=\$__tilde_prefix" 2>/dev/null || true
|
|
134
|
+
if [[ -n "$__tilde_expanded" && "$__tilde_expanded" != "$__tilde_prefix" ]]; then
|
|
135
|
+
__glob_current="\${__tilde_expanded}\${pattern#\$__tilde_prefix}"
|
|
136
|
+
else
|
|
137
|
+
__tilde_prefix=""
|
|
138
|
+
fi
|
|
139
|
+
fi
|
|
140
|
+
# If the glob base is a directory without a trailing slash,
|
|
141
|
+
# append one so that the glob enumerates its contents
|
|
142
|
+
if [[ -d "$__glob_current" && "$__glob_current" != */ ]]; then
|
|
143
|
+
__glob_current="$__glob_current/"
|
|
144
|
+
fi
|
|
145
|
+
fi
|
|
146
|
+
fi
|
|
147
|
+
|
|
148
|
+
# Enable dotglob when hidden files are requested, or when the user
|
|
149
|
+
# is already navigating inside a hidden directory (e.g., ~/.config/nvim/)
|
|
150
|
+
# This runs after tilde expansion so that paths like ~/.config/ are
|
|
151
|
+
# checked against the expanded path, not the literal ~ string.
|
|
152
|
+
local __inside_hidden_path=0
|
|
153
|
+
case "/\${__glob_current%/}/" in
|
|
154
|
+
*/.[!.]*/*|*/..?*/*) __inside_hidden_path=1 ;;
|
|
155
|
+
esac
|
|
156
|
+
# Also check if the current prefix explicitly targets hidden entries
|
|
157
|
+
# (e.g., user typed "." or ".e" to complete .env)
|
|
158
|
+
local __prefix_targets_hidden=0
|
|
159
|
+
local __prefix_base="\${__glob_current##*/}"
|
|
160
|
+
[[ "$__prefix_base" == .* ]] && __prefix_targets_hidden=1
|
|
161
|
+
if [[ "$hidden" == "1" || "$__inside_hidden_path" == "1" || "$__prefix_targets_hidden" == "1" ]]; then shopt -s dotglob; fi
|
|
162
|
+
|
|
163
|
+
# Pre-expand file candidates. When the glob base came from the
|
|
164
|
+
# program's pattern and contains wildcard characters (* or ?),
|
|
165
|
+
# use it as-is via compgen -G (safe — no command substitution).
|
|
166
|
+
# Otherwise append * to treat it as a prefix.
|
|
167
|
+
# Note: [ is NOT treated as a glob indicator because it commonly
|
|
168
|
+
# appears in literal filenames like [draft] or foo[1].txt.
|
|
169
|
+
local -a __candidates=()
|
|
170
|
+
if [[ "$__from_pattern" == "1" && "$__glob_current" == *[\*\?]* ]]; then
|
|
171
|
+
mapfile -t __candidates < <(compgen -G "$__glob_current" 2>/dev/null)
|
|
172
|
+
# For file/any modes, also include directories from the base
|
|
173
|
+
# directory for navigation even when the glob itself does not
|
|
174
|
+
# match directory names. Skip this for directory mode so that
|
|
175
|
+
# the pattern's basename filter is respected.
|
|
176
|
+
if [[ "$type" != "directory" ]]; then
|
|
177
|
+
local __glob_dir="\${__glob_current%/*}"
|
|
178
|
+
[[ "$__glob_dir" == "$__glob_current" ]] && __glob_dir="."
|
|
179
|
+
local __d
|
|
180
|
+
for __d in "$__glob_dir"/*/; do
|
|
181
|
+
[[ -d "$__d" ]] && __candidates+=("\${__d%/}")
|
|
182
|
+
done
|
|
183
|
+
fi
|
|
184
|
+
else
|
|
185
|
+
__candidates=("$__glob_current"*)
|
|
186
|
+
# Remove no-match sentinel (bash returns the literal pattern).
|
|
187
|
+
# Also check -L for dangling symlinks which are valid candidates.
|
|
188
|
+
if [[ \${#__candidates[@]} -eq 1 && ! -e "\${__candidates[0]}" && ! -L "\${__candidates[0]}" ]]; then
|
|
189
|
+
__candidates=()
|
|
190
|
+
fi
|
|
191
|
+
fi
|
|
51
192
|
|
|
52
193
|
# Generate file completions based on type
|
|
53
194
|
case "$type" in
|
|
54
195
|
file)
|
|
55
|
-
# Complete files
|
|
196
|
+
# Complete files and directories (directories for navigation)
|
|
56
197
|
if [[ -n "$extensions" ]]; then
|
|
57
|
-
#
|
|
198
|
+
# Files with extension filtering + directories
|
|
58
199
|
local ext_pattern="\${extensions//,/|}"
|
|
59
|
-
for
|
|
60
|
-
[[ -
|
|
200
|
+
for item in "\${__candidates[@]}"; do
|
|
201
|
+
if [[ -d "$item" ]]; then
|
|
202
|
+
COMPREPLY+=("$item/")
|
|
203
|
+
elif [[ -f "$item" && "$item" =~ \\.($ext_pattern)$ ]]; then
|
|
204
|
+
COMPREPLY+=("$item")
|
|
205
|
+
fi
|
|
61
206
|
done
|
|
62
207
|
else
|
|
63
|
-
# Complete files
|
|
64
|
-
|
|
65
|
-
[[ -
|
|
66
|
-
|
|
208
|
+
# Complete files and directories for navigation
|
|
209
|
+
for item in "\${__candidates[@]}"; do
|
|
210
|
+
if [[ -d "$item" ]]; then
|
|
211
|
+
COMPREPLY+=("$item/")
|
|
212
|
+
elif [[ -f "$item" ]]; then
|
|
213
|
+
COMPREPLY+=("$item")
|
|
214
|
+
fi
|
|
215
|
+
done
|
|
67
216
|
fi
|
|
68
217
|
;;
|
|
69
218
|
directory)
|
|
70
219
|
# Complete directories only
|
|
71
|
-
|
|
72
|
-
COMPREPLY+=("$dir/")
|
|
73
|
-
done
|
|
220
|
+
for dir in "\${__candidates[@]}"; do
|
|
221
|
+
[[ -d "$dir" ]] && COMPREPLY+=("$dir/")
|
|
222
|
+
done
|
|
74
223
|
;;
|
|
75
224
|
any)
|
|
76
225
|
# Complete both files and directories
|
|
@@ -78,31 +227,50 @@ function _${programName} () {
|
|
|
78
227
|
# Files with extension filtering + directories
|
|
79
228
|
# Files with extension filtering
|
|
80
229
|
local ext_pattern="\${extensions//,/|}"
|
|
81
|
-
for item in "
|
|
230
|
+
for item in "\${__candidates[@]}"; do
|
|
82
231
|
if [[ -d "$item" ]]; then
|
|
83
232
|
COMPREPLY+=("$item/")
|
|
84
|
-
elif [[ -
|
|
233
|
+
elif [[ ( -e "$item" || -L "$item" ) && "$item" =~ \\.($ext_pattern)$ ]]; then
|
|
85
234
|
COMPREPLY+=("$item")
|
|
86
235
|
fi
|
|
87
236
|
done
|
|
88
237
|
else
|
|
89
238
|
# Complete files and directories, add slash to directories
|
|
90
|
-
|
|
239
|
+
for item in "\${__candidates[@]}"; do
|
|
91
240
|
if [[ -d "$item" ]]; then
|
|
92
241
|
COMPREPLY+=("$item/")
|
|
93
|
-
|
|
242
|
+
# Use -e || -L to include non-regular files (sockets, FIFOs, dangling symlinks)
|
|
243
|
+
elif [[ -e "$item" || -L "$item" ]]; then
|
|
94
244
|
COMPREPLY+=("$item")
|
|
95
245
|
fi
|
|
96
|
-
done
|
|
246
|
+
done
|
|
97
247
|
fi
|
|
98
248
|
;;
|
|
99
249
|
esac
|
|
100
250
|
|
|
251
|
+
# Restore tilde prefix in completion results
|
|
252
|
+
if [[ -n "$__tilde_prefix" ]]; then
|
|
253
|
+
local __i
|
|
254
|
+
for __i in "\${!COMPREPLY[@]}"; do
|
|
255
|
+
COMPREPLY[\$__i]="\${COMPREPLY[\$__i]/#\$__tilde_expanded/\$__tilde_prefix}"
|
|
256
|
+
done
|
|
257
|
+
fi
|
|
258
|
+
|
|
259
|
+
# Restore glob/shell options
|
|
260
|
+
# Restore GLOBIGNORE before dotglob because assigning GLOBIGNORE
|
|
261
|
+
# implicitly enables dotglob in Bash
|
|
262
|
+
if [[ "$__globignore_was_set" == "1" ]]; then GLOBIGNORE="$__saved_globignore"; fi
|
|
263
|
+
if [[ "$__dotglob_was_set" == "0" ]]; then shopt -u dotglob; else shopt -s dotglob; fi
|
|
264
|
+
if [[ "$__failglob_was_set" == "1" ]]; then shopt -s failglob; fi
|
|
265
|
+
if [[ "$__noglob_was_set" == "1" ]]; then set -f; fi
|
|
266
|
+
|
|
101
267
|
# Filter out hidden files unless requested
|
|
102
|
-
if [[ "$hidden" != "1" && "$
|
|
268
|
+
if [[ "$hidden" != "1" && "$__inside_hidden_path" == "0" && "$__prefix_targets_hidden" == "0" ]]; then
|
|
103
269
|
local filtered=()
|
|
270
|
+
local __name
|
|
104
271
|
for item in "\${COMPREPLY[@]}"; do
|
|
105
|
-
|
|
272
|
+
__name="\${item%/}"; __name="\${__name##*/}"
|
|
273
|
+
[[ "$__name" != .* ]] && filtered+=("$item")
|
|
106
274
|
done
|
|
107
275
|
COMPREPLY=("\${filtered[@]}")
|
|
108
276
|
fi
|
|
@@ -113,18 +281,19 @@ function _${programName} () {
|
|
|
113
281
|
done < <(${programName} ${escapedArgs} "\${prev[@]}" "$current" 2>/dev/null)
|
|
114
282
|
}
|
|
115
283
|
|
|
116
|
-
complete -F _${programName} ${programName}
|
|
284
|
+
complete -F _${programName} -- ${programName}
|
|
117
285
|
`;
|
|
118
286
|
},
|
|
119
287
|
*encodeSuggestions(suggestions) {
|
|
120
288
|
let i = 0;
|
|
121
289
|
for (const suggestion of suggestions) {
|
|
122
290
|
if (i > 0) yield "\n";
|
|
123
|
-
if (suggestion.kind === "literal") yield
|
|
291
|
+
if (suggestion.kind === "literal") yield sanitizeForTransport(suggestion.text);
|
|
124
292
|
else {
|
|
125
|
-
const extensions = suggestion.extensions
|
|
293
|
+
const extensions = encodeExtensions(suggestion.extensions);
|
|
126
294
|
const hidden = suggestion.includeHidden ? "1" : "0";
|
|
127
|
-
|
|
295
|
+
const pattern = encodePattern(suggestion.pattern ?? "");
|
|
296
|
+
yield `__FILE__:${suggestion.type}:${extensions}:${pattern}:${hidden}`;
|
|
128
297
|
}
|
|
129
298
|
i++;
|
|
130
299
|
}
|
|
@@ -139,7 +308,7 @@ const zsh = {
|
|
|
139
308
|
generateScript(programName, args = []) {
|
|
140
309
|
validateProgramName(programName);
|
|
141
310
|
const escapedArgs = args.map((arg) => `'${arg.replace(/'/g, "'\\''")}'`).join(" ");
|
|
142
|
-
return
|
|
311
|
+
return `#compdef ${programName}
|
|
143
312
|
function _${programName.replace(/[^a-zA-Z0-9]/g, "_")} () {
|
|
144
313
|
local current="\$words[CURRENT]"
|
|
145
314
|
local -a prev
|
|
@@ -176,17 +345,52 @@ function _${programName.replace(/[^a-zA-Z0-9]/g, "_")} () {
|
|
|
176
345
|
# Parse file completion directive: __FILE__:type:extensions:pattern:hidden
|
|
177
346
|
local type extensions pattern hidden
|
|
178
347
|
IFS=':' read -r _ type extensions pattern hidden <<< "\$value"
|
|
348
|
+
pattern="\${pattern//%3A/:}"; pattern="\${pattern//%25/%}"
|
|
179
349
|
has_file_completion=1
|
|
180
350
|
|
|
351
|
+
# Native completion helpers expect normal zsh glob grouping even if
|
|
352
|
+
# the caller enabled SH_GLOB in their shell options.
|
|
353
|
+
local __was_sh_glob=0
|
|
354
|
+
[[ -o sh_glob ]] && __was_sh_glob=1
|
|
355
|
+
unsetopt sh_glob
|
|
356
|
+
|
|
357
|
+
# Enable glob_dots when hidden files are requested so that
|
|
358
|
+
# _files and _directories include dot-prefixed entries
|
|
359
|
+
local __was_glob_dots=0
|
|
360
|
+
[[ -o glob_dots ]] && __was_glob_dots=1
|
|
361
|
+
if [[ "\$hidden" == "1" ]]; then setopt glob_dots; fi
|
|
362
|
+
|
|
363
|
+
# When a pattern is specified, override PREFIX so that _files and
|
|
364
|
+
# _directories enumerate the pattern's directory instead of the
|
|
365
|
+
# current word. If the user has already typed beyond the pattern,
|
|
366
|
+
# keep PREFIX unchanged for incremental narrowing.
|
|
367
|
+
local __saved_prefix="\$PREFIX"
|
|
368
|
+
if [[ -n "\$pattern" ]]; then
|
|
369
|
+
# Normalize leading ./ so that ./src/ma matches pattern src/
|
|
370
|
+
local __norm_prefix="\${PREFIX#./}"
|
|
371
|
+
local __norm_pattern="\${pattern#./}"
|
|
372
|
+
local __compare_pattern="\$__norm_pattern"
|
|
373
|
+
if [[ "\$__compare_pattern" == *[\*\?]* ]]; then
|
|
374
|
+
__compare_pattern="\${__compare_pattern%%[\*\?]*}"
|
|
375
|
+
[[ "\$__compare_pattern" == */* ]] && __compare_pattern="\${__compare_pattern%/*}/" || __compare_pattern=""
|
|
376
|
+
fi
|
|
377
|
+
if [[ ( -n "\$__compare_pattern" || -n "\$__norm_prefix" ) && \${#__norm_prefix} -ge \${#__compare_pattern} && "\${__norm_prefix[1,\${#__compare_pattern}]}" == "\$__compare_pattern" && "\$PREFIX" != "\$pattern" ]]; then
|
|
378
|
+
# User typed an equivalent or extended form — keep PREFIX
|
|
379
|
+
true
|
|
380
|
+
else
|
|
381
|
+
PREFIX="\$pattern"
|
|
382
|
+
fi
|
|
383
|
+
fi
|
|
384
|
+
|
|
181
385
|
# Use zsh's native file completion
|
|
182
386
|
case "\$type" in
|
|
183
387
|
file)
|
|
184
388
|
if [[ -n "\$extensions" ]]; then
|
|
185
|
-
# Complete files with extension filtering
|
|
186
|
-
local ext_pattern="*.(
|
|
187
|
-
_files -g "
|
|
389
|
+
# Complete files with extension filtering + directories for navigation
|
|
390
|
+
local ext_pattern="*.(\$\{extensions//,/|\})"
|
|
391
|
+
_files -g "\$ext_pattern"; _directories
|
|
188
392
|
else
|
|
189
|
-
_files
|
|
393
|
+
_files
|
|
190
394
|
fi
|
|
191
395
|
;;
|
|
192
396
|
directory)
|
|
@@ -195,16 +399,18 @@ function _${programName.replace(/[^a-zA-Z0-9]/g, "_")} () {
|
|
|
195
399
|
any)
|
|
196
400
|
if [[ -n "\$extensions" ]]; then
|
|
197
401
|
# Complete both files and directories, with extension filtering for files
|
|
198
|
-
local ext_pattern="*.(
|
|
199
|
-
_files -g "
|
|
402
|
+
local ext_pattern="*.(\$\{extensions//,/|\})"
|
|
403
|
+
_files -g "\$ext_pattern"; _directories
|
|
200
404
|
else
|
|
201
405
|
_files
|
|
202
406
|
fi
|
|
203
407
|
;;
|
|
204
408
|
esac
|
|
205
409
|
|
|
206
|
-
#
|
|
207
|
-
|
|
410
|
+
# Restore PREFIX and glob_dots to their previous state
|
|
411
|
+
PREFIX="\$__saved_prefix"
|
|
412
|
+
if [[ "\$__was_sh_glob" == "1" ]]; then setopt sh_glob; else unsetopt sh_glob; fi
|
|
413
|
+
if [[ "\$__was_glob_dots" == "1" ]]; then setopt glob_dots; else unsetopt glob_dots; fi
|
|
208
414
|
else
|
|
209
415
|
# Regular literal completion
|
|
210
416
|
if [[ -n "\$value" ]]; then
|
|
@@ -236,13 +442,14 @@ compdef _${programName.replace(/[^a-zA-Z0-9]/g, "_")} ${programName}
|
|
|
236
442
|
},
|
|
237
443
|
*encodeSuggestions(suggestions) {
|
|
238
444
|
for (const suggestion of suggestions) if (suggestion.kind === "literal") {
|
|
239
|
-
const description = suggestion.description == null ? "" : formatMessage(suggestion.description, { colors: false });
|
|
240
|
-
yield `${suggestion.text}\0${description}\0`;
|
|
445
|
+
const description = suggestion.description == null ? "" : sanitizeForTransport(formatMessage(suggestion.description, { colors: false }));
|
|
446
|
+
yield `${sanitizeForTransport(suggestion.text)}\0${description}\0`;
|
|
241
447
|
} else {
|
|
242
|
-
const extensions = suggestion.extensions
|
|
448
|
+
const extensions = encodeExtensions(suggestion.extensions);
|
|
243
449
|
const hidden = suggestion.includeHidden ? "1" : "0";
|
|
244
|
-
const description = suggestion.description == null ? "" : formatMessage(suggestion.description, { colors: false });
|
|
245
|
-
|
|
450
|
+
const description = suggestion.description == null ? "" : sanitizeForTransport(formatMessage(suggestion.description, { colors: false }));
|
|
451
|
+
const pattern = encodePattern(suggestion.pattern ?? "");
|
|
452
|
+
yield `__FILE__:${suggestion.type}:${extensions}:${pattern}:${hidden}\0${description}\0`;
|
|
246
453
|
}
|
|
247
454
|
}
|
|
248
455
|
};
|
|
@@ -274,44 +481,190 @@ ${escapedArgs ? ` set -l output (${programName} ${escapedArgs} $prev $current
|
|
|
274
481
|
for line in $output
|
|
275
482
|
if string match -q '__FILE__:*' -- $line
|
|
276
483
|
# Parse file completion directive: __FILE__:type:extensions:pattern:hidden
|
|
277
|
-
set -l
|
|
484
|
+
set -l directive (string split \\t -- $line)[1]
|
|
485
|
+
set -l parts (string split ':' -- $directive)
|
|
278
486
|
set -l type $parts[2]
|
|
279
487
|
set -l extensions $parts[3]
|
|
280
|
-
set -l pattern $parts[4]
|
|
488
|
+
set -l pattern (string replace -a '%25' '%' -- (string replace -a '%3A' ':' -- $parts[4]))
|
|
281
489
|
set -l hidden $parts[5]
|
|
282
490
|
|
|
491
|
+
# When a pattern is specified, use it as the glob base instead
|
|
492
|
+
# of the current word. If the user has already typed beyond the
|
|
493
|
+
# pattern (e.g., pattern="src/" and current="src/ma"), keep the
|
|
494
|
+
# current word for incremental narrowing.
|
|
495
|
+
set -l glob_base $current
|
|
496
|
+
set -l __tilde_prefix ""
|
|
497
|
+
set -l __from_pattern 0
|
|
498
|
+
if test -n "$pattern"
|
|
499
|
+
# Normalize leading ./ so that ./src/ma matches pattern src/
|
|
500
|
+
set -l __norm_current (string replace -r '^\\./' '' -- "$current")
|
|
501
|
+
set -l __norm_pattern (string replace -r '^\\./' '' -- "$pattern")
|
|
502
|
+
# For wildcard patterns, compare only the directory prefix
|
|
503
|
+
set -l __compare_pattern "$__norm_pattern"
|
|
504
|
+
if string match -q '*[*?]*' -- "$__compare_pattern"
|
|
505
|
+
set __compare_pattern (string replace -r '[*?].*' '' -- "$__compare_pattern")
|
|
506
|
+
if string match -q '*/*' -- "$__compare_pattern"
|
|
507
|
+
set __compare_pattern (string replace -r '/[^/]*$' '/' -- "$__compare_pattern")
|
|
508
|
+
else
|
|
509
|
+
set __compare_pattern ""
|
|
510
|
+
end
|
|
511
|
+
end
|
|
512
|
+
set -l __cp_len (string length -- "$__compare_pattern")
|
|
513
|
+
set -l __nc_len (string length -- "$__norm_current")
|
|
514
|
+
if begin; test -n "$__compare_pattern"; or test -n "$__norm_current"; end
|
|
515
|
+
and test $__nc_len -ge $__cp_len
|
|
516
|
+
and test (string sub -l $__cp_len -- "$__norm_current") = "$__compare_pattern"
|
|
517
|
+
and test "$current" != "$pattern"
|
|
518
|
+
set glob_base $current
|
|
519
|
+
else
|
|
520
|
+
set glob_base $pattern
|
|
521
|
+
set __from_pattern 1
|
|
522
|
+
end
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
# Expand tilde prefix for globbing — fish does not expand ~
|
|
526
|
+
# inside variable substitutions, so replace it with $HOME
|
|
527
|
+
if string match -q '~/*' -- "$glob_base"
|
|
528
|
+
set __tilde_prefix "~"
|
|
529
|
+
set glob_base (string replace -r '^~' "$HOME" -- "$glob_base")
|
|
530
|
+
else if string match -q '~' -- "$glob_base"
|
|
531
|
+
set __tilde_prefix "~"
|
|
532
|
+
set glob_base "$HOME/"
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
# If the glob base is a directory without a trailing slash,
|
|
536
|
+
# append one so that the glob enumerates its contents
|
|
537
|
+
if test -d "$glob_base"; and not string match -q '*/' -- "$glob_base"
|
|
538
|
+
set glob_base "$glob_base/"
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
# Pre-expand file candidates. When the glob base came from
|
|
542
|
+
# the program's pattern and contains * or ?, use it as the
|
|
543
|
+
# complete glob expression. Otherwise append * as a prefix.
|
|
544
|
+
# Note: [ is NOT treated as a glob because it commonly
|
|
545
|
+
# appears in literal filenames like [draft] or foo[1].txt.
|
|
546
|
+
set -l __has_glob 0
|
|
547
|
+
if test $__from_pattern -eq 1
|
|
548
|
+
and string match -q '*[*?]*' -- "$glob_base"
|
|
549
|
+
set __has_glob 1
|
|
550
|
+
end
|
|
551
|
+
set -l __candidates
|
|
552
|
+
if test $__has_glob -eq 1
|
|
553
|
+
# Safe glob expansion without eval: split the pattern into
|
|
554
|
+
# directory and filter parts, list the directory, then use
|
|
555
|
+
# string match for wildcard filtering
|
|
556
|
+
set -l __glob_dir (string replace -r '/[^/]*$' '' -- "$glob_base")
|
|
557
|
+
set -l __glob_filter (string replace -r '.*/' '' -- "$glob_base")
|
|
558
|
+
if test -z "$__glob_dir"; or test "$__glob_dir" = "$glob_base"
|
|
559
|
+
set __glob_dir "."
|
|
560
|
+
end
|
|
561
|
+
# Match files by the glob filter. Fish's * does not match
|
|
562
|
+
# dotfiles, so also scan .* when the filter targets them.
|
|
563
|
+
for __item in $__glob_dir/*
|
|
564
|
+
if test -e "$__item"
|
|
565
|
+
set -l __bn (basename "$__item")
|
|
566
|
+
if string match -q "$__glob_filter" -- "$__bn"
|
|
567
|
+
set -a __candidates "$__item"
|
|
568
|
+
end
|
|
569
|
+
end
|
|
570
|
+
end
|
|
571
|
+
if string match -q '.*' -- "$__glob_filter"
|
|
572
|
+
for __item in $__glob_dir/.*
|
|
573
|
+
if test -e "$__item"
|
|
574
|
+
set -l __bn (basename "$__item")
|
|
575
|
+
if test "$__bn" = "." -o "$__bn" = ".."
|
|
576
|
+
continue
|
|
577
|
+
end
|
|
578
|
+
if string match -q "$__glob_filter" -- "$__bn"
|
|
579
|
+
set -a __candidates "$__item"
|
|
580
|
+
end
|
|
581
|
+
end
|
|
582
|
+
end
|
|
583
|
+
end
|
|
584
|
+
# For file/any modes, also include directories for navigation.
|
|
585
|
+
# Skip for directory mode so the pattern filter is respected.
|
|
586
|
+
if test "$type" != "directory"
|
|
587
|
+
for __item in $__glob_dir/*/
|
|
588
|
+
if test -d "$__item"
|
|
589
|
+
set -a __candidates (string replace -r '/$' '' -- "$__item")
|
|
590
|
+
end
|
|
591
|
+
end
|
|
592
|
+
end
|
|
593
|
+
else
|
|
594
|
+
set __candidates $glob_base*
|
|
595
|
+
end
|
|
596
|
+
|
|
283
597
|
# Generate file completions based on type
|
|
284
598
|
set -l items
|
|
285
599
|
switch $type
|
|
286
600
|
case file
|
|
287
|
-
# Complete files
|
|
288
|
-
for item in $
|
|
289
|
-
if test -
|
|
601
|
+
# Complete files and directories (directories for navigation)
|
|
602
|
+
for item in $__candidates
|
|
603
|
+
if test -d $item
|
|
604
|
+
set -a items $item/
|
|
605
|
+
else if test -f $item
|
|
290
606
|
set -a items $item
|
|
291
607
|
end
|
|
292
608
|
end
|
|
609
|
+
# Fish's * glob does not match dotfiles; add them
|
|
610
|
+
# explicitly when the basename is empty (i.e., $glob_base
|
|
611
|
+
# is "" or ends with "/"), because only then are * and
|
|
612
|
+
# .* complementary. When a non-empty basename is present
|
|
613
|
+
# (e.g., "foo"), foo* already covers foo.txt, so foo.*
|
|
614
|
+
# would just produce duplicates.
|
|
615
|
+
if test "$hidden" = "1" -a $__has_glob -eq 0
|
|
616
|
+
if test -z "$glob_base"; or string match -q '*/' -- "$glob_base"
|
|
617
|
+
for item in $glob_base.*
|
|
618
|
+
if test -d $item
|
|
619
|
+
set -a items $item/
|
|
620
|
+
else if test -f $item
|
|
621
|
+
set -a items $item
|
|
622
|
+
end
|
|
623
|
+
end
|
|
624
|
+
end
|
|
625
|
+
end
|
|
293
626
|
case directory
|
|
294
627
|
# Complete directories only
|
|
295
|
-
for item in $
|
|
628
|
+
for item in $__candidates
|
|
296
629
|
if test -d $item
|
|
297
630
|
set -a items $item/
|
|
298
631
|
end
|
|
299
632
|
end
|
|
633
|
+
if test "$hidden" = "1" -a $__has_glob -eq 0
|
|
634
|
+
if test -z "$glob_base"; or string match -q '*/' -- "$glob_base"
|
|
635
|
+
for item in $glob_base.*
|
|
636
|
+
if test -d $item
|
|
637
|
+
set -a items $item/
|
|
638
|
+
end
|
|
639
|
+
end
|
|
640
|
+
end
|
|
641
|
+
end
|
|
300
642
|
case any
|
|
301
643
|
# Complete both files and directories
|
|
302
|
-
for item in $
|
|
644
|
+
for item in $__candidates
|
|
303
645
|
if test -d $item
|
|
304
646
|
set -a items $item/
|
|
305
647
|
else if test -f $item
|
|
306
648
|
set -a items $item
|
|
307
649
|
end
|
|
308
650
|
end
|
|
651
|
+
if test "$hidden" = "1" -a $__has_glob -eq 0
|
|
652
|
+
if test -z "$glob_base"; or string match -q '*/' -- "$glob_base"
|
|
653
|
+
for item in $glob_base.*
|
|
654
|
+
if test -d $item
|
|
655
|
+
set -a items $item/
|
|
656
|
+
else if test -f $item
|
|
657
|
+
set -a items $item
|
|
658
|
+
end
|
|
659
|
+
end
|
|
660
|
+
end
|
|
661
|
+
end
|
|
309
662
|
end
|
|
310
663
|
|
|
311
664
|
# Filter by extensions if specified
|
|
312
665
|
if test -n "$extensions" -a "$type" != directory
|
|
313
666
|
set -l filtered
|
|
314
|
-
set -l ext_list (string split ',' -- $extensions)
|
|
667
|
+
set -l ext_list (string replace -r '^\\.' '' -- (string split ',' -- $extensions))
|
|
315
668
|
for item in $items
|
|
316
669
|
# Skip directories, they don't have extensions
|
|
317
670
|
if string match -q '*/' -- $item
|
|
@@ -329,8 +682,11 @@ ${escapedArgs ? ` set -l output (${programName} ${escapedArgs} $prev $current
|
|
|
329
682
|
set items $filtered
|
|
330
683
|
end
|
|
331
684
|
|
|
332
|
-
# Filter out hidden files unless requested
|
|
333
|
-
|
|
685
|
+
# Filter out hidden files unless requested.
|
|
686
|
+
# Check the basename of glob_base so that patterns like
|
|
687
|
+
# "src/.e" correctly target hidden entries.
|
|
688
|
+
set -l __glob_basename (string replace -r '.*/(.*)' '$1' -- $glob_base)
|
|
689
|
+
if test "$hidden" != "1" -a (string sub -l 1 -- $__glob_basename) != "."
|
|
334
690
|
set -l filtered
|
|
335
691
|
for item in $items
|
|
336
692
|
set -l basename (basename $item)
|
|
@@ -341,6 +697,15 @@ ${escapedArgs ? ` set -l output (${programName} ${escapedArgs} $prev $current
|
|
|
341
697
|
set items $filtered
|
|
342
698
|
end
|
|
343
699
|
|
|
700
|
+
# Restore tilde prefix in completion results
|
|
701
|
+
if test -n "$__tilde_prefix"
|
|
702
|
+
set -l restored
|
|
703
|
+
for item in $items
|
|
704
|
+
set -a restored (string replace "$HOME" "~" -- $item)
|
|
705
|
+
end
|
|
706
|
+
set items $restored
|
|
707
|
+
end
|
|
708
|
+
|
|
344
709
|
# Output file completions
|
|
345
710
|
for item in $items
|
|
346
711
|
echo $item
|
|
@@ -367,13 +732,14 @@ complete -c ${programName} -f -a '(${functionName})'
|
|
|
367
732
|
for (const suggestion of suggestions) {
|
|
368
733
|
if (i > 0) yield "\n";
|
|
369
734
|
if (suggestion.kind === "literal") {
|
|
370
|
-
const description = suggestion.description == null ? "" : formatMessage(suggestion.description, { colors: false });
|
|
371
|
-
yield `${suggestion.text}\t${description}`;
|
|
735
|
+
const description = suggestion.description == null ? "" : sanitizeForTransport(formatMessage(suggestion.description, { colors: false }));
|
|
736
|
+
yield `${sanitizeForTransport(suggestion.text)}\t${description}`;
|
|
372
737
|
} else {
|
|
373
|
-
const extensions = suggestion.extensions
|
|
738
|
+
const extensions = encodeExtensions(suggestion.extensions);
|
|
374
739
|
const hidden = suggestion.includeHidden ? "1" : "0";
|
|
375
|
-
const description = suggestion.description == null ? "" : formatMessage(suggestion.description, { colors: false });
|
|
376
|
-
|
|
740
|
+
const description = suggestion.description == null ? "" : sanitizeForTransport(formatMessage(suggestion.description, { colors: false }));
|
|
741
|
+
const pattern = encodePattern(suggestion.pattern ?? "");
|
|
742
|
+
yield `__FILE__:${suggestion.type}:${extensions}:${pattern}:${hidden}\t${description}`;
|
|
377
743
|
}
|
|
378
744
|
i++;
|
|
379
745
|
}
|
|
@@ -479,10 +845,11 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
|
|
|
479
845
|
$output | lines | each {|line|
|
|
480
846
|
if ($line | str starts-with '__FILE__:') {
|
|
481
847
|
# Parse file completion directive: __FILE__:type:extensions:pattern:hidden
|
|
482
|
-
let
|
|
848
|
+
let directive = ($line | split row "\t" | first)
|
|
849
|
+
let parts = ($directive | split row ':')
|
|
483
850
|
let type = ($parts | get 1)
|
|
484
851
|
let extensions = ($parts | get 2)
|
|
485
|
-
let pattern = ($parts | get 3)
|
|
852
|
+
let pattern = ($parts | get 3 | str replace -a '%3A' ':' | str replace -a '%25' '%')
|
|
486
853
|
let hidden = ($parts | get 4) == '1'
|
|
487
854
|
|
|
488
855
|
# Extract prefix from the last argument if it exists
|
|
@@ -492,33 +859,96 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
|
|
|
492
859
|
""
|
|
493
860
|
}
|
|
494
861
|
|
|
495
|
-
#
|
|
496
|
-
#
|
|
497
|
-
|
|
862
|
+
# When a pattern is specified, use it as the glob base instead of
|
|
863
|
+
# the user-typed prefix. If the user has already typed beyond the
|
|
864
|
+
# pattern (e.g., pattern="src/" and prefix="src/ma"), keep the
|
|
865
|
+
# prefix for incremental narrowing. Normalize path separators
|
|
866
|
+
# before comparing so that Windows backslashes match forward slashes
|
|
867
|
+
# in the transported pattern.
|
|
868
|
+
let glob_base = if ($pattern | is-not-empty) {
|
|
869
|
+
# Normalize separators and leading ./; downcase only on Windows
|
|
870
|
+
# where filesystems are typically case-insensitive
|
|
871
|
+
let norm_prefix_raw = ($prefix | str replace -a '\\' '/' | str replace -r '^\\./' '')
|
|
872
|
+
let norm_pattern_raw = ($pattern | str replace -a '\\' '/' | str replace -r '^\\./' '')
|
|
873
|
+
let is_win = (($nu.os-info.name | str downcase) == "windows")
|
|
874
|
+
let norm_prefix = (if $is_win { $norm_prefix_raw | str downcase } else { $norm_prefix_raw })
|
|
875
|
+
let norm_pattern = (if $is_win { $norm_pattern_raw | str downcase } else { $norm_pattern_raw })
|
|
876
|
+
# For wildcard patterns, compare only the directory prefix
|
|
877
|
+
let compare_pattern = if ($norm_pattern =~ '[*?]') {
|
|
878
|
+
let before_wild = ($norm_pattern | str replace -r '[*?].*' '')
|
|
879
|
+
if ($before_wild =~ '/') { $before_wild | str replace -r '/[^/]*$' '/' } else { "" }
|
|
880
|
+
} else {
|
|
881
|
+
$norm_pattern
|
|
882
|
+
}
|
|
883
|
+
if (($compare_pattern | is-not-empty) or ($norm_prefix | is-not-empty)) and ($norm_prefix | str starts-with $compare_pattern) and (($norm_prefix | str length) >= ($compare_pattern | str length)) and ($prefix != $pattern) {
|
|
884
|
+
$prefix
|
|
885
|
+
} else {
|
|
886
|
+
$pattern
|
|
887
|
+
}
|
|
888
|
+
} else {
|
|
889
|
+
$prefix
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
# If the glob base is a directory without a trailing slash (e.g., "src"),
|
|
893
|
+
# append one so that ls enumerates its contents instead of matching
|
|
894
|
+
# siblings like "src-old"
|
|
895
|
+
let glob_base = if ($glob_base | is-not-empty) and ($glob_base | path type) == "dir" and (not ($glob_base | str ends-with "/")) {
|
|
896
|
+
$glob_base + "/"
|
|
897
|
+
} else {
|
|
898
|
+
$glob_base
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
# Generate file completions based on type.
|
|
902
|
+
# When the glob base contains wildcard characters, use it as-is;
|
|
903
|
+
# otherwise append * to treat it as a prefix.
|
|
904
|
+
# Note: into glob is required so that ls expands wildcards from a variable
|
|
905
|
+
let has_glob = ($glob_base =~ '[*?]')
|
|
906
|
+
let ls_pattern = if ($glob_base | is-empty) {
|
|
907
|
+
"."
|
|
908
|
+
} else if $has_glob {
|
|
909
|
+
($glob_base | into glob)
|
|
910
|
+
} else {
|
|
911
|
+
($glob_base + "*" | into glob)
|
|
912
|
+
}
|
|
498
913
|
|
|
914
|
+
# When using a glob pattern, also compute a directory listing pattern
|
|
915
|
+
# so that file/any modes can include directories for navigation
|
|
916
|
+
let glob_dir_pattern = if $has_glob {
|
|
917
|
+
let dir_part = ($glob_base | path dirname)
|
|
918
|
+
let dir = if ($dir_part | is-empty) or ($dir_part == $glob_base) { "." } else { $dir_part }
|
|
919
|
+
($dir + "/*" | into glob)
|
|
920
|
+
} else {
|
|
921
|
+
null
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
# Use ls -a to include hidden files when requested
|
|
499
925
|
let items = try {
|
|
500
926
|
match $type {
|
|
501
927
|
"file" => {
|
|
502
928
|
if ($extensions | is-empty) {
|
|
503
|
-
ls $ls_pattern | where type == file
|
|
929
|
+
(if $hidden { ls -a $ls_pattern } else { ls $ls_pattern }) | where type == file or type == dir
|
|
504
930
|
} else {
|
|
505
|
-
let ext_list = ($extensions | split row ',')
|
|
506
|
-
ls $ls_pattern
|
|
931
|
+
let ext_list = ($extensions | split row ',' | each {|e| $e | str replace -r '^\\.' '' })
|
|
932
|
+
let all_items = (if $hidden { ls -a $ls_pattern } else { ls $ls_pattern })
|
|
933
|
+
let dirs = $all_items | where type == dir
|
|
934
|
+
let files = $all_items | where type == file | where {|f|
|
|
507
935
|
let ext = ($f.name | path parse | get extension)
|
|
508
936
|
$ext in $ext_list
|
|
509
937
|
}
|
|
938
|
+
$dirs | append $files
|
|
510
939
|
}
|
|
511
940
|
},
|
|
512
941
|
"directory" => {
|
|
513
|
-
ls $ls_pattern | where type == dir
|
|
942
|
+
(if $hidden { ls -a $ls_pattern } else { ls $ls_pattern }) | where type == dir
|
|
514
943
|
},
|
|
515
944
|
"any" => {
|
|
516
945
|
if ($extensions | is-empty) {
|
|
517
|
-
ls $ls_pattern
|
|
946
|
+
if $hidden { ls -a $ls_pattern } else { ls $ls_pattern }
|
|
518
947
|
} else {
|
|
519
|
-
let ext_list = ($extensions | split row ',')
|
|
520
|
-
let
|
|
521
|
-
let
|
|
948
|
+
let ext_list = ($extensions | split row ',' | each {|e| $e | str replace -r '^\\.' '' })
|
|
949
|
+
let all_items = (if $hidden { ls -a $ls_pattern } else { ls $ls_pattern })
|
|
950
|
+
let dirs = $all_items | where type == dir
|
|
951
|
+
let files = $all_items | where type == file | where {|f|
|
|
522
952
|
let ext = ($f.name | path parse | get extension)
|
|
523
953
|
$ext in $ext_list
|
|
524
954
|
}
|
|
@@ -530,8 +960,25 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
|
|
|
530
960
|
[]
|
|
531
961
|
}
|
|
532
962
|
|
|
533
|
-
#
|
|
534
|
-
|
|
963
|
+
# When using a glob pattern, also add directories from the base
|
|
964
|
+
# directory for navigation (file/any modes should list directories
|
|
965
|
+
# even when the glob itself does not match directory names)
|
|
966
|
+
let items = if ($glob_dir_pattern != null) and ($type != "directory") {
|
|
967
|
+
let extra_dirs = try {
|
|
968
|
+
(if $hidden { ls -a $glob_dir_pattern } else { ls $glob_dir_pattern }) | where type == dir
|
|
969
|
+
} catch {
|
|
970
|
+
[]
|
|
971
|
+
}
|
|
972
|
+
$items | append $extra_dirs | uniq-by name
|
|
973
|
+
} else {
|
|
974
|
+
$items
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
# Filter out hidden files unless requested.
|
|
978
|
+
# Check the basename of glob_base so that patterns like "src/.e"
|
|
979
|
+
# correctly target hidden entries.
|
|
980
|
+
let glob_basename = ($glob_base | path basename)
|
|
981
|
+
let filtered = if $hidden or ($glob_basename | str starts-with '.') {
|
|
535
982
|
$items
|
|
536
983
|
} else {
|
|
537
984
|
$items | where {|item|
|
|
@@ -540,12 +987,22 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
|
|
|
540
987
|
}
|
|
541
988
|
}
|
|
542
989
|
|
|
990
|
+
# Extract directory prefix to preserve in completion text
|
|
991
|
+
let dir_prefix = if ($glob_base | is-empty) {
|
|
992
|
+
""
|
|
993
|
+
} else if ($glob_base | str ends-with "/") {
|
|
994
|
+
$glob_base
|
|
995
|
+
} else {
|
|
996
|
+
let parsed = ($glob_base | path parse)
|
|
997
|
+
if ($parsed.parent | is-empty) { "" } else if ($parsed.parent | str ends-with "/") { $parsed.parent } else { $parsed.parent + "/" }
|
|
998
|
+
}
|
|
999
|
+
|
|
543
1000
|
# Format file completions
|
|
544
1001
|
$filtered | each {|item|
|
|
545
1002
|
let name = if $item.type == dir {
|
|
546
|
-
($item.name | path basename) + "/"
|
|
1003
|
+
$dir_prefix + ($item.name | path basename) + "/"
|
|
547
1004
|
} else {
|
|
548
|
-
$item.name | path basename
|
|
1005
|
+
$dir_prefix + ($item.name | path basename)
|
|
549
1006
|
}
|
|
550
1007
|
{ value: $name }
|
|
551
1008
|
}
|
|
@@ -597,13 +1054,14 @@ ${functionName}-external
|
|
|
597
1054
|
for (const suggestion of suggestions) {
|
|
598
1055
|
if (i > 0) yield "\n";
|
|
599
1056
|
if (suggestion.kind === "literal") {
|
|
600
|
-
const description = suggestion.description == null ? "" : formatMessage(suggestion.description, { colors: false });
|
|
601
|
-
yield `${suggestion.text}\t${description}`;
|
|
1057
|
+
const description = suggestion.description == null ? "" : sanitizeForTransport(formatMessage(suggestion.description, { colors: false }));
|
|
1058
|
+
yield `${sanitizeForTransport(suggestion.text)}\t${description}`;
|
|
602
1059
|
} else {
|
|
603
|
-
const extensions = suggestion.extensions
|
|
1060
|
+
const extensions = encodeExtensions(suggestion.extensions);
|
|
604
1061
|
const hidden = suggestion.includeHidden ? "1" : "0";
|
|
605
|
-
const description = suggestion.description == null ? "" : formatMessage(suggestion.description, { colors: false });
|
|
606
|
-
|
|
1062
|
+
const description = suggestion.description == null ? "" : sanitizeForTransport(formatMessage(suggestion.description, { colors: false }));
|
|
1063
|
+
const pattern = encodePattern(suggestion.pattern ?? "");
|
|
1064
|
+
yield `__FILE__:${suggestion.type}:${extensions}:${pattern}:${hidden}\t${description}`;
|
|
607
1065
|
}
|
|
608
1066
|
i++;
|
|
609
1067
|
}
|
|
@@ -669,59 +1127,110 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
|
|
|
669
1127
|
|
|
670
1128
|
if (\$line -match '^__FILE__:') {
|
|
671
1129
|
# Parse file completion directive: __FILE__:type:extensions:pattern:hidden
|
|
672
|
-
\$
|
|
1130
|
+
\$directive = (\$line -split "\`t")[0]
|
|
1131
|
+
\$parts = \$directive -split ':', 5
|
|
673
1132
|
\$type = \$parts[1]
|
|
674
1133
|
\$extensions = \$parts[2]
|
|
675
|
-
\$pattern = \$parts[3]
|
|
1134
|
+
\$pattern = \$parts[3] -replace '%3A', ':' -replace '%25', '%'
|
|
676
1135
|
\$hidden = \$parts[4] -eq '1'
|
|
677
1136
|
|
|
678
|
-
#
|
|
679
|
-
|
|
1137
|
+
# When a pattern is specified, use it as the file matching
|
|
1138
|
+
# prefix instead of the current word. If the user has
|
|
1139
|
+
# already typed beyond the pattern, keep their input for
|
|
1140
|
+
# incremental narrowing. Normalize path separators before
|
|
1141
|
+
# comparing so that Windows backslashes match forward slashes
|
|
1142
|
+
# in the transported pattern.
|
|
1143
|
+
\$normalizedPattern = if (\$pattern) { \$pattern.Replace('\\', '/') -replace '^\\./','' } else { '' }
|
|
1144
|
+
\$normalizedWord = if (\$wordToComplete) { \$wordToComplete.Replace('\\', '/') -replace '^\\./','' } else { '' }
|
|
1145
|
+
\$comparison = if (\$IsWindows) { [System.StringComparison]::OrdinalIgnoreCase } else { [System.StringComparison]::Ordinal }
|
|
1146
|
+
# For wildcard patterns, compare only the directory prefix
|
|
1147
|
+
\$comparePattern = \$normalizedPattern
|
|
1148
|
+
if (\$comparePattern -match '[\*\?]') {
|
|
1149
|
+
\$beforeWild = \$comparePattern -replace '[\*\?].*', ''
|
|
1150
|
+
if (\$beforeWild.Contains('/')) {
|
|
1151
|
+
\$comparePattern = \$beforeWild.Substring(0, \$beforeWild.LastIndexOf('/') + 1)
|
|
1152
|
+
} else {
|
|
1153
|
+
\$comparePattern = ''
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
\$prefix = if ((\$comparePattern -or \$normalizedWord) -and \$normalizedWord -and \$normalizedWord.StartsWith(\$comparePattern, \$comparison) -and \$normalizedWord.Length -ge \$comparePattern.Length -and \$wordToComplete -ne \$pattern) {
|
|
1157
|
+
\$wordToComplete
|
|
1158
|
+
} elseif (\$pattern) {
|
|
1159
|
+
\$pattern
|
|
1160
|
+
} elseif (\$wordToComplete) {
|
|
1161
|
+
\$wordToComplete
|
|
1162
|
+
} else { '' }
|
|
1163
|
+
|
|
1164
|
+
# Use -Force to include hidden files when requested, or when
|
|
1165
|
+
# the prefix basename targets dotfiles (e.g., src/.e)
|
|
1166
|
+
\$prefixBasename = if (\$prefix) { Split-Path -Leaf \$prefix } else { '' }
|
|
1167
|
+
\$forceParam = if (\$hidden -or (\$prefixBasename -and \$prefixBasename.StartsWith('.'))) { @{Force = \$true} } else { @{} }
|
|
1168
|
+
|
|
1169
|
+
# If prefix is a directory without trailing slash, append
|
|
1170
|
+
# one so Get-ChildItem lists its contents
|
|
1171
|
+
if (\$prefix -and (Test-Path -Path \$prefix -PathType Container) -and -not \$prefix.EndsWith('/') -and -not \$prefix.EndsWith('\\')) {
|
|
1172
|
+
\$prefix = \$prefix + '/'
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
# Build the glob path — when the prefix contains wildcard
|
|
1176
|
+
# characters, use it as-is; otherwise append *
|
|
1177
|
+
\$globPath = if (\$prefix -match '[\*\?]') { \$prefix } else { "\${prefix}*" }
|
|
680
1178
|
|
|
681
1179
|
# Get file system items based on type
|
|
682
1180
|
\$items = @()
|
|
683
1181
|
switch (\$type) {
|
|
684
1182
|
'file' {
|
|
685
1183
|
if (\$extensions) {
|
|
686
|
-
# Filter by extensions
|
|
687
|
-
\$extList = \$extensions -split ','
|
|
688
|
-
\$items = Get-ChildItem
|
|
1184
|
+
# Filter by extensions, always include directories
|
|
1185
|
+
\$extList = (\$extensions -split ',') -replace '^\\.',''
|
|
1186
|
+
\$items = Get-ChildItem @forceParam -Path \$globPath -ErrorAction SilentlyContinue |
|
|
689
1187
|
Where-Object {
|
|
690
|
-
\$
|
|
691
|
-
\$extList | ForEach-Object { if (\$ext -eq ".\$_") { return \$true } }
|
|
1188
|
+
\$_.PSIsContainer -or ((\$_.Extension -replace '^\\.','' ) -in \$extList)
|
|
692
1189
|
}
|
|
693
1190
|
} else {
|
|
694
|
-
\$items = Get-ChildItem
|
|
1191
|
+
\$items = Get-ChildItem @forceParam -Path \$globPath -ErrorAction SilentlyContinue
|
|
695
1192
|
}
|
|
696
1193
|
}
|
|
697
1194
|
'directory' {
|
|
698
|
-
\$items = Get-ChildItem -Directory -Path
|
|
1195
|
+
\$items = Get-ChildItem @forceParam -Directory -Path \$globPath -ErrorAction SilentlyContinue
|
|
699
1196
|
}
|
|
700
1197
|
'any' {
|
|
701
1198
|
if (\$extensions) {
|
|
702
|
-
#
|
|
703
|
-
\$
|
|
704
|
-
\$
|
|
705
|
-
\$files = Get-ChildItem -File -Path "\${prefix}*" -ErrorAction SilentlyContinue |
|
|
1199
|
+
# Filter by extensions, always include directories
|
|
1200
|
+
\$extList = (\$extensions -split ',') -replace '^\\.',''
|
|
1201
|
+
\$items = Get-ChildItem @forceParam -Path \$globPath -ErrorAction SilentlyContinue |
|
|
706
1202
|
Where-Object {
|
|
707
|
-
\$
|
|
708
|
-
\$extList | ForEach-Object { if (\$ext -eq ".\$_") { return \$true } }
|
|
1203
|
+
\$_.PSIsContainer -or ((\$_.Extension -replace '^\\.','' ) -in \$extList)
|
|
709
1204
|
}
|
|
710
|
-
\$items = \$dirs + \$files
|
|
711
1205
|
} else {
|
|
712
|
-
\$items = Get-ChildItem -Path
|
|
1206
|
+
\$items = Get-ChildItem @forceParam -Path \$globPath -ErrorAction SilentlyContinue
|
|
713
1207
|
}
|
|
714
1208
|
}
|
|
715
1209
|
}
|
|
716
1210
|
|
|
717
|
-
#
|
|
718
|
-
|
|
1211
|
+
# For file/any modes with glob patterns, also add directories
|
|
1212
|
+
# from the base directory for navigation
|
|
1213
|
+
if (\$prefix -match '[\*\?]' -and \$type -ne 'directory') {
|
|
1214
|
+
\$globDir = Split-Path -Parent \$prefix
|
|
1215
|
+
if (-not \$globDir) { \$globDir = '.' }
|
|
1216
|
+
\$extraDirs = Get-ChildItem @forceParam -Directory -Path "\$globDir/*" -ErrorAction SilentlyContinue
|
|
1217
|
+
if (\$extraDirs) { \$items = @(\$items) + @(\$extraDirs) | Select-Object -Unique }
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
# Filter hidden files unless requested or the prefix targets dotfiles
|
|
1221
|
+
if (-not \$hidden -and -not (\$prefixBasename -and \$prefixBasename.StartsWith('.'))) {
|
|
719
1222
|
\$items = \$items | Where-Object { -not \$_.Attributes.HasFlag([System.IO.FileAttributes]::Hidden) }
|
|
720
1223
|
}
|
|
721
1224
|
|
|
1225
|
+
# Extract directory prefix to preserve in completion text
|
|
1226
|
+
\$dirPrefix = if (\$prefix -and (\$prefix.Contains('/') -or \$prefix.Contains('\\'))) {
|
|
1227
|
+
\$slashIdx = [Math]::Max(\$prefix.LastIndexOf('/'), \$prefix.LastIndexOf('\\'))
|
|
1228
|
+
\$prefix.Substring(0, \$slashIdx + 1)
|
|
1229
|
+
} else { '' }
|
|
1230
|
+
|
|
722
1231
|
# Create completion results for files
|
|
723
1232
|
\$items | ForEach-Object {
|
|
724
|
-
\$completionText = if (\$_.PSIsContainer) { "\$(\$_.Name)/" } else { \$_.Name }
|
|
1233
|
+
\$completionText = if (\$_.PSIsContainer) { "\$dirPrefix\$(\$_.Name)/" } else { "\$dirPrefix\$(\$_.Name)" }
|
|
725
1234
|
\$itemType = if (\$_.PSIsContainer) { 'Directory' } else { 'File' }
|
|
726
1235
|
[System.Management.Automation.CompletionResult]::new(
|
|
727
1236
|
\$completionText,
|
|
@@ -756,13 +1265,15 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
|
|
|
756
1265
|
for (const suggestion of suggestions) {
|
|
757
1266
|
if (i > 0) yield "\n";
|
|
758
1267
|
if (suggestion.kind === "literal") {
|
|
759
|
-
const description = suggestion.description == null ? "" : formatMessage(suggestion.description, { colors: false });
|
|
760
|
-
|
|
1268
|
+
const description = suggestion.description == null ? "" : sanitizeForTransport(formatMessage(suggestion.description, { colors: false }));
|
|
1269
|
+
const text = sanitizeForTransport(suggestion.text);
|
|
1270
|
+
yield `${text}\t${text}\t${description}`;
|
|
761
1271
|
} else {
|
|
762
|
-
const extensions = suggestion.extensions
|
|
1272
|
+
const extensions = encodeExtensions(suggestion.extensions);
|
|
763
1273
|
const hidden = suggestion.includeHidden ? "1" : "0";
|
|
764
|
-
const description = suggestion.description == null ? "" : formatMessage(suggestion.description, { colors: false });
|
|
765
|
-
|
|
1274
|
+
const description = suggestion.description == null ? "" : sanitizeForTransport(formatMessage(suggestion.description, { colors: false }));
|
|
1275
|
+
const pattern = encodePattern(suggestion.pattern ?? "");
|
|
1276
|
+
yield `__FILE__:${suggestion.type}:${extensions}:${pattern}:${hidden}\t[file]\t${description}`;
|
|
766
1277
|
}
|
|
767
1278
|
i++;
|
|
768
1279
|
}
|