pub_just/
completions.rs

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];