Skip to main content

git_stk/
completions.rs

1use std::env;
2use std::ffi::OsStr;
3use std::io::{self, Write};
4
5use anyhow::{Context, Result, bail};
6use clap_complete::Shell;
7use clap_complete::engine::CompletionCandidate;
8use clap_complete::env::Shells;
9
10use crate::{git, stack};
11
12/// Environment variable that triggers dynamic completion (see `main`).
13pub const COMPLETE_VAR: &str = "COMPLETE";
14
15/// Lets git's bash completion handle `git stk <TAB>` by delegating to the
16/// dynamic clap completer, which only knows the `git-stk` binary form. The
17/// current word is forwarded so prefix filtering keeps working.
18const BASH_GIT_SHIM: &str = r#"
19_git_stk() {
20    local cur="${COMP_WORDS[COMP_CWORD]}"
21    COMP_WORDS=("git-stk" "${COMP_WORDS[@]:2}")
22    COMP_CWORD=$((COMP_CWORD - 1))
23    _clap_complete_git_stk git-stk "$cur" ""
24}
25"#;
26
27/// Same bridge for zsh: its `_git` dispatcher calls `_git-stk` for
28/// `git stk <TAB>`. zsh's dynamic scoping makes the rewritten `words` and
29/// `CURRENT` visible to the clap completer.
30const ZSH_GIT_SHIM: &str = r#"
31function _git-stk() {
32    local -a words=("git-stk" "${words[@]:2}")
33    local CURRENT=$((CURRENT - 1))
34    _clap_dynamic_completer_git_stk
35}
36"#;
37
38pub fn print(shell: Shell) -> Result<()> {
39    // Point the registration at this exact binary so completion works even
40    // when git-stk is not on PATH (and stays correct across upgrades, since
41    // shells re-source this output on every start).
42    let completer = env::current_exe()
43        .ok()
44        .and_then(|path| path.to_str().map(str::to_owned))
45        .unwrap_or_else(|| "git-stk".to_owned());
46
47    write(shell, &completer, &mut io::stdout().lock())
48}
49
50/// Write the dynamic-completion registration script for `shell`, with shims
51/// so the `git stk` subcommand form completes too. `completer` is the binary
52/// the shell invokes at completion time.
53pub fn write(shell: Shell, completer: &str, writer: &mut dyn Write) -> Result<()> {
54    let shells = Shells::builtins();
55    let name = shell.to_string();
56    let Some(env_completer) = shells.completer(&name) else {
57        bail!("no dynamic completion support for {name}");
58    };
59
60    env_completer
61        .write_registration(COMPLETE_VAR, "git-stk", "git-stk", completer, writer)
62        .with_context(|| format!("failed to write {name} completion registration"))?;
63
64    match shell {
65        Shell::Bash => write!(writer, "{BASH_GIT_SHIM}")?,
66        Shell::Zsh => write!(writer, "{ZSH_GIT_SHIM}")?,
67        _ => {}
68    }
69
70    Ok(())
71}
72
73/// Complete branch-name arguments with local branches.
74pub fn branch_candidates(current: &OsStr) -> Vec<CompletionCandidate> {
75    let Some(prefix) = current.to_str() else {
76        return Vec::new();
77    };
78
79    git::local_branches()
80        .unwrap_or_default()
81        .into_iter()
82        .filter(|branch| branch.starts_with(prefix))
83        .map(CompletionCandidate::new)
84        .collect()
85}
86
87/// Complete `up` with the current branch's stack children only.
88pub fn child_branch_candidates(current: &OsStr) -> Vec<CompletionCandidate> {
89    let Some(prefix) = current.to_str() else {
90        return Vec::new();
91    };
92    let Ok(branch) = git::current_branch() else {
93        return Vec::new();
94    };
95
96    stack::children_for_branch(&branch)
97        .unwrap_or_default()
98        .into_iter()
99        .filter(|child| child.starts_with(prefix))
100        .map(CompletionCandidate::new)
101        .collect()
102}