1use super::*;
2
3#[derive(ValueEnum, Debug, Clone, Copy, PartialEq)]
4pub enum Shell {
5 Bash,
6 Elvish,
7 Fish,
8 #[value(alias = "nu")]
9 Nushell,
10 Powershell,
11 Zsh,
12}
13
14impl Shell {
15 pub fn script(self) -> RunResult<'static, String> {
16 match self {
17 Self::Bash => completions::clap(clap_complete::Shell::Bash),
18 Self::Elvish => completions::clap(clap_complete::Shell::Elvish),
19 Self::Fish => completions::clap(clap_complete::Shell::Fish),
20 Self::Nushell => Ok(completions::NUSHELL_COMPLETION_SCRIPT.into()),
21 Self::Powershell => completions::clap(clap_complete::Shell::PowerShell),
22 Self::Zsh => completions::clap(clap_complete::Shell::Zsh),
23 }
24 }
25}
26
27fn clap(shell: clap_complete::Shell) -> RunResult<'static, String> {
28 fn replace(haystack: &mut String, needle: &str, replacement: &str) -> RunResult<'static, ()> {
29 if let Some(index) = haystack.find(needle) {
30 haystack.replace_range(index..index + needle.len(), replacement);
31 Ok(())
32 } else {
33 Err(Error::internal(format!(
34 "Failed to find text:\n{needle}\n…in completion script:\n{haystack}"
35 )))
36 }
37 }
38
39 let mut script = {
40 let mut tempfile = tempfile().map_err(|io_error| Error::TempfileIo { io_error })?;
41
42 clap_complete::generate(
43 shell,
44 &mut crate::config::Config::app(),
45 env!("CARGO_PKG_NAME"),
46 &mut tempfile,
47 );
48
49 tempfile
50 .rewind()
51 .map_err(|io_error| Error::TempfileIo { io_error })?;
52
53 let mut buffer = String::new();
54
55 tempfile
56 .read_to_string(&mut buffer)
57 .map_err(|io_error| Error::TempfileIo { io_error })?;
58
59 buffer
60 };
61
62 match shell {
63 clap_complete::Shell::Bash => {
64 for (needle, replacement) in completions::BASH_COMPLETION_REPLACEMENTS {
65 replace(&mut script, needle, replacement)?;
66 }
67 }
68 clap_complete::Shell::Fish => {
69 script.insert_str(0, completions::FISH_RECIPE_COMPLETIONS);
70 }
71 clap_complete::Shell::PowerShell => {
72 for (needle, replacement) in completions::POWERSHELL_COMPLETION_REPLACEMENTS {
73 replace(&mut script, needle, replacement)?;
74 }
75 }
76 clap_complete::Shell::Zsh => {
77 for (needle, replacement) in completions::ZSH_COMPLETION_REPLACEMENTS {
78 replace(&mut script, needle, replacement)?;
79 }
80 }
81 _ => {}
82 }
83
84 Ok(script.trim().into())
85}
86
87const NUSHELL_COMPLETION_SCRIPT: &str = r#"def "nu-complete just" [] {
88 (^just --dump --unstable --dump-format json | from json).recipes | transpose recipe data | flatten | where {|row| $row.private == false } | select recipe doc parameters | rename value description
89}
90
91# Just: A Command Runner
92export extern "just" [
93 ...recipe: string@"nu-complete just", # Recipe(s) to run, may be with argument(s)
94]"#;
95
96const FISH_RECIPE_COMPLETIONS: &str = r#"function __fish_just_complete_recipes
97 if string match -rq '(-f|--justfile)\s*=?(?<justfile>[^\s]+)' -- (string split -- ' -- ' (commandline -pc))[1]
98 set -fx JUST_JUSTFILE "$justfile"
99 end
100 just --list 2> /dev/null | tail -n +2 | awk '{
101 command = $1;
102 args = $0;
103 desc = "";
104 delim = "";
105 sub(/^[[:space:]]*[^[:space:]]*/, "", args);
106 gsub(/^[[:space:]]+|[[:space:]]+$/, "", args);
107
108 if (match(args, /#.*/)) {
109 desc = substr(args, RSTART+2, RLENGTH);
110 args = substr(args, 0, RSTART-1);
111 gsub(/^[[:space:]]+|[[:space:]]+$/, "", args);
112 }
113
114 gsub(/\+|=[`\'"][^`\'"]*[`\'"]/, "", args);
115 gsub(/ /, ",", args);
116
117 if (args != ""){
118 args = "Args: " args;
119 }
120
121 if (args != "" && desc != "") {
122 delim = "; ";
123 }
124
125 print command "\t" args delim desc
126 }'
127end
128
129# don't suggest files right off
130complete -c just -n "__fish_is_first_arg" --no-files
131
132# complete recipes
133complete -c just -a '(__fish_just_complete_recipes)'
134
135# autogenerated completions
136"#;
137
138const ZSH_COMPLETION_REPLACEMENTS: &[(&str, &str)] = &[
139 (
140 r#" _arguments "${_arguments_options[@]}" : \"#,
141 r" local common=(",
142 ),
143 (
144 r"'*--set=[Override <VARIABLE> with <VALUE>]:VARIABLE:_default:VARIABLE:_default' \",
145 r"'*--set=[Override <VARIABLE> with <VALUE>]: :(_just_variables)' \",
146 ),
147 (
148 r"'()-s+[Show recipe at <PATH>]:PATH:_default' \
149'()--show=[Show recipe at <PATH>]:PATH:_default' \",
150 r"'-s+[Show recipe at <PATH>]: :(_just_commands)' \
151'--show=[Show recipe at <PATH>]: :(_just_commands)' \",
152 ),
153 (
154 "'*::ARGUMENTS -- Overrides and recipe(s) to run, defaulting to the first recipe in the \
155 justfile:_default' \\
156&& ret=0",
157 r#")
158
159 _arguments "${_arguments_options[@]}" $common \
160 '1: :_just_commands' \
161 '*: :->args' \
162 && ret=0
163
164 case $state in
165 args)
166 curcontext="${curcontext%:*}-${words[2]}:"
167
168 local lastarg=${words[${#words}]}
169 local recipe
170
171 local cmds; cmds=(
172 ${(s: :)$(_call_program commands just --summary)}
173 )
174
175 # Find first recipe name
176 for ((i = 2; i < $#words; i++ )) do
177 if [[ ${cmds[(I)${words[i]}]} -gt 0 ]]; then
178 recipe=${words[i]}
179 break
180 fi
181 done
182
183 if [[ $lastarg = */* ]]; then
184 # Arguments contain slash would be recognised as a file
185 _arguments -s -S $common '*:: :_files'
186 elif [[ $lastarg = *=* ]]; then
187 # Arguments contain equal would be recognised as a variable
188 _message "value"
189 elif [[ $recipe ]]; then
190 # Show usage message
191 _message "`just --show $recipe`"
192 # Or complete with other commands
193 #_arguments -s -S $common '*:: :_just_commands'
194 else
195 _arguments -s -S $common '*:: :_just_commands'
196 fi
197 ;;
198 esac
199
200 return ret
201"#,
202 ),
203 (
204 " local commands; commands=()",
205 r#" [[ $PREFIX = -* ]] && return 1
206 integer ret=1
207 local variables; variables=(
208 ${(s: :)$(_call_program commands just --variables)}
209 )
210 local commands; commands=(
211 ${${${(M)"${(f)$(_call_program commands just --list)}":# *}/ ##/}/ ##/:Args: }
212 )
213"#,
214 ),
215 (
216 r#" _describe -t commands 'just commands' commands "$@""#,
217 r#" if compset -P '*='; then
218 case "${${words[-1]%=*}#*=}" in
219 *) _message 'value' && ret=0 ;;
220 esac
221 else
222 _describe -t variables 'variables' variables -qS "=" && ret=0
223 _describe -t commands 'just commands' commands "$@"
224 fi
225"#,
226 ),
227 (
228 r#"_just "$@""#,
229 r#"(( $+functions[_just_variables] )) ||
230_just_variables() {
231 [[ $PREFIX = -* ]] && return 1
232 integer ret=1
233 local variables; variables=(
234 ${(s: :)$(_call_program commands just --variables)}
235 )
236
237 if compset -P '*='; then
238 case "${${words[-1]%=*}#*=}" in
239 *) _message 'value' && ret=0 ;;
240 esac
241 else
242 _describe -t variables 'variables' variables && ret=0
243 fi
244
245 return ret
246}
247
248_just "$@""#,
249 ),
250];
251
252const POWERSHELL_COMPLETION_REPLACEMENTS: &[(&str, &str)] = &[(
253 r#"$completions.Where{ $_.CompletionText -like "$wordToComplete*" } |
254 Sort-Object -Property ListItemText"#,
255 r#"function Get-JustFileRecipes([string[]]$CommandElements) {
256 $justFileIndex = $commandElements.IndexOf("--justfile");
257
258 if ($justFileIndex -ne -1 -and $justFileIndex + 1 -le $commandElements.Length) {
259 $justFileLocation = $commandElements[$justFileIndex + 1]
260 }
261
262 $justArgs = @("--summary")
263
264 if (Test-Path $justFileLocation) {
265 $justArgs += @("--justfile", $justFileLocation)
266 }
267
268 $recipes = $(just @justArgs) -split ' '
269 return $recipes | ForEach-Object { [CompletionResult]::new($_) }
270 }
271
272 $elementValues = $commandElements | Select-Object -ExpandProperty Value
273 $recipes = Get-JustFileRecipes -CommandElements $elementValues
274 $completions += $recipes
275 $completions.Where{ $_.CompletionText -like "$wordToComplete*" } |
276 Sort-Object -Property ListItemText"#,
277)];
278
279const BASH_COMPLETION_REPLACEMENTS: &[(&str, &str)] = &[
280 (
281 r#" if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
282 COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
283 return 0
284 fi"#,
285 r#" if [[ ${cur} == -* ]] ; then
286 COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
287 return 0
288 elif [[ ${COMP_CWORD} -eq 1 ]]; then
289 local recipes=$(just --summary 2> /dev/null)
290
291 if echo "${cur}" | \grep -qF '/'; then
292 local path_prefix=$(echo "${cur}" | sed 's/[/][^/]*$/\//')
293 local recipes=$(just --summary 2> /dev/null -- "${path_prefix}")
294 local recipes=$(printf "${path_prefix}%s\t" $recipes)
295 fi
296
297 if [[ $? -eq 0 ]]; then
298 COMPREPLY=( $(compgen -W "${recipes}" -- "${cur}") )
299 return 0
300 fi
301 fi"#,
302 ),
303 (
304 r"local i cur prev opts cmd",
305 r"local i cur prev words cword opts cmd",
306 ),
307 (
308 r#" cur="${COMP_WORDS[COMP_CWORD]}"
309 prev="${COMP_WORDS[COMP_CWORD-1]}""#,
310 r#"
311 # Modules use "::" as the separator, which is considered a wordbreak character in bash.
312 # The _get_comp_words_by_ref function is a hack to allow for exceptions to this rule without
313 # modifying the global COMP_WORDBREAKS environment variable.
314 if type _get_comp_words_by_ref &>/dev/null; then
315 _get_comp_words_by_ref -n : cur prev words cword
316 else
317 cur="${COMP_WORDS[COMP_CWORD]}"
318 prev="${COMP_WORDS[COMP_CWORD-1]}"
319 words=$COMP_WORDS
320 cword=$COMP_CWORD
321 fi
322"#,
323 ),
324 (r"for i in ${COMP_WORDS[@]}", r"for i in ${words[@]}"),
325 (
326 r"elif [[ ${COMP_CWORD} -eq 1 ]]; then",
327 r"elif [[ ${cword} -eq 1 ]]; then",
328 ),
329 (
330 r#"COMPREPLY=( $(compgen -W "${recipes}" -- "${cur}") )"#,
331 r#"COMPREPLY=( $(compgen -W "${recipes}" -- "${cur}") )
332 if type __ltrim_colon_completions &>/dev/null; then
333 __ltrim_colon_completions "$cur"
334 fi"#,
335 ),
336];