use crate::cli::Cli;
use eyre::Result;
use std::ffi::OsString;
use strum::EnumString;
pub(crate) fn completion_request(argv: &[OsString]) -> Option<String> {
let request = usage_rs::complete::CompletionRequest::parse(argv)?;
if request.candidates_for.is_some() {
return Cli::completion_request(argv);
}
let spec = super::usage::completion_spec();
complete_spec(&spec, &request)
.ok()
.or_else(|| Cli::completion_request(argv))
}
pub(crate) fn usage_spec_request(argv: &[OsString]) -> Option<Result<String>> {
if argv
.first()
.is_none_or(|arg| arg != "__usage_complete_word")
{
return None;
}
Some((|| {
let path = argv
.get(1)
.and_then(|arg| arg.to_str())
.ok_or_else(|| eyre::eyre!("missing completion specification"))?;
let path = crate::packslip::completions::decode_spec_path(path)?;
let spec = crate::file::read_to_string(path)?
.parse::<usage::Spec>()
.map_err(|err| eyre::eyre!("invalid usage specification: {err}"))?;
let request_argv: Vec<_> = std::iter::once(OsString::from("__complete_word__"))
.chain(argv.iter().skip(2).cloned())
.collect();
let request = usage_rs::complete::CompletionRequest::parse(&request_argv)
.ok_or_else(|| eyre::eyre!("invalid completion request"))?;
complete_spec(&spec, &request)
})())
}
fn complete_spec(
spec: &usage::Spec,
request: &usage_rs::complete::CompletionRequest,
) -> Result<String> {
let answer = usage_cli::complete_answer(
spec,
&request.split.words,
request.split.cword,
request.shell.as_str(),
)
.map_err(|err| eyre::eyre!("{err}"))?;
let candidates = if answer.files {
vec![]
} else {
answer
.candidates
.into_iter()
.map(|(value, description)| {
if description.is_empty() {
usage_rs::complete::Candidate::new(value)
} else {
usage_rs::complete::Candidate::described(value, description)
}
})
.collect()
};
let answer = usage_rs::complete::Completions {
candidates,
files: answer.files.then_some(usage_rs::complete::Files::Any),
};
Ok(usage_rs::complete::render(&answer, request.shell))
}
#[derive(Debug, usage_rs::Args)]
#[usage(aliases = ["complete", "completions"], verbatim_doc_comment, example(r###"mise completion zsh --install
mise completion bash --install
mise completion fish --install
mise completion powershell --install"###, help = r###"Install for your shell; follow any printed one-time setup instructions"###),
example(r###"mise completion zsh"###, help = r###"Print a completion script to inspect or save at a custom path"###),
example(r###"mise completion zsh --tool rg
mise completion zsh --tool rg --install"###, help = r###"For a tool installed through Packslip with completion resources"###))]
pub(crate) struct Completion {
#[usage(required_unless = "shell_type", value_enum)]
shell: Option<Shell>,
#[usage(long = "shell", short = 's', hide = true, value_enum)]
shell_type: Option<Shell>,
#[usage(long, verbatim_doc_comment)]
include_bash_completion_lib: bool,
#[usage(long, verbatim_doc_comment, hide = true)]
usage: bool,
#[usage(long, verbatim_doc_comment, effect = "write")]
install: bool,
#[usage(long, requires = "--install", effect = "write")]
force: bool,
#[usage(long, verbatim_doc_comment)]
tool: Option<String>,
}
impl Completion {
pub(crate) async fn run(self) -> Result<()> {
let shell = self.shell.or(self.shell_type).unwrap();
if let Some(tool) = &self.tool {
if self.install {
return self.install_tool_stub(tool, shell.into());
}
let config = crate::config::Config::get().await?;
let script =
crate::packslip::completion_script(&config, tool, shell.packslip_name()).await?;
miseprintln!("{}", script.trim());
return Ok(());
}
if self.install {
return self.install_script(shell.into());
}
let script = Cli::completion_script(shell.into());
miseprintln!("{}", script.trim());
Ok(())
}
fn install_tool_stub(&self, tool: &str, shell: usage_rs::complete::Shell) -> Result<()> {
use usage_rs::install::{self, OnForeign};
if !crate::file::is_plain_file_name(tool) {
eyre::bail!(
"--install takes the executable's name, not a tool id; run `mise completion {} --tool {tool}` to see what the id resolves to",
shell.as_str()
);
}
let stub = crate::packslip::stub(tool, shell)?;
let on_foreign = if self.force {
OnForeign::Overwrite
} else {
OnForeign::Refuse
};
let plan = install::plan_for("mise", tool, shell, &install::Env::from_process())
.map_err(eyre::Report::new)?;
let done = install::write(&plan, &stub, on_foreign).map_err(|err| match &err {
install::Error::Foreign { .. } => eyre::eyre!(
"{err}\n\nPass --force to replace it, or redirect `mise completion {} --tool {tool}` yourself.",
shell.as_str()
),
_ => eyre::Report::new(err),
})?;
Self::report_install(&done);
Ok(())
}
fn report_install(done: &usage_rs::install::Installed) {
use usage_rs::install::{self, Wrote};
eprintln!("installing to {}", done.plan.path.display());
if done.wrote == Wrote::Unchanged {
eprintln!("already up to date");
}
if let Some(line) = done.plan.loading.instruction() {
let file = match &done.plan.loading {
install::Loading::Manual { file, .. } => file.as_str(),
_ => "your shell's startup file",
};
eprintln!("\nadd this to {file}, once:\n\n{line}\n");
}
if let Some(note) = done.plan.note {
eprintln!("note: {note}");
}
}
fn install_script(&self, shell: usage_rs::complete::Shell) -> Result<()> {
use usage_rs::install::{self, OnForeign};
let on_foreign = if self.force {
OnForeign::Overwrite
} else {
OnForeign::Refuse
};
let done = Cli::install_completion(shell, &install::Env::from_process(), on_foreign)
.map_err(|err| match &err {
install::Error::Foreign { .. } => eyre::eyre!(
"{err}\n\nPass --force to replace it, or redirect the script yourself."
),
_ => eyre::Report::new(err),
})?;
Self::report_install(&done);
Ok(())
}
}
#[derive(Debug, Clone, Copy, EnumString, strum::Display, usage_rs::ValueEnum)]
#[strum(serialize_all = "snake_case")]
#[usage(rename_all = "snake_case")]
enum Shell {
Bash,
Fish,
#[strum(serialize = "powershell")]
#[usage(name = "powershell", visible_alias = "pwsh")]
Powershell,
Zsh,
}
impl Shell {
fn packslip_name(self) -> &'static str {
match self {
Shell::Bash => "bash",
Shell::Fish => "fish",
Shell::Powershell => "powershell",
Shell::Zsh => "zsh",
}
}
}
impl From<Shell> for usage_rs::complete::Shell {
fn from(shell: Shell) -> Self {
match shell {
Shell::Bash => Self::Bash,
Shell::Fish => Self::Fish,
Shell::Powershell => Self::PowerShell,
Shell::Zsh => Self::Zsh,
}
}
}
#[cfg(test)]
mod shell_name_tests {
use super::*;
use usage_rs::spec::ValueEnum;
#[test]
fn usage_spec_completes_from_a_native_path() {
let dir = tempfile::tempdir().unwrap();
#[cfg(all(unix, not(target_os = "macos")))]
let name = {
use std::os::unix::ffi::OsStringExt;
OsString::from_vec(b"spec with spaces-\xff.kdl".to_vec())
};
#[cfg(any(windows, target_os = "macos"))]
let name = OsString::from("spec with spaces-\u{03bb}.kdl");
let path = dir.path().join(name);
std::fs::write(&path, "name \"probe\"\nflag \"--from-spec\"\n").unwrap();
let encoded = crate::packslip::completions::encode_spec_path(&path);
let argv: Vec<OsString> = [
"__usage_complete_word",
&encoded,
"--shell",
"bash",
"--line",
"probe --from",
]
.into_iter()
.map(OsString::from)
.collect();
let answer = usage_spec_request(&argv).unwrap().unwrap();
assert!(answer.contains("--from-spec"), "{answer}");
}
#[test]
fn pwsh_is_accepted_as_powershell() {
assert!(matches!(
<Shell as ValueEnum>::from_choice("pwsh"),
Some(Shell::Powershell)
));
assert!(matches!(
<Shell as ValueEnum>::from_choice("powershell"),
Some(Shell::Powershell)
));
}
#[test]
fn the_primary_names_are_unchanged() {
let listed: Vec<&str> = Shell::DETAILS.iter().map(|choice| choice.value).collect();
assert_eq!(listed, ["bash", "fish", "powershell", "zsh"]);
}
#[test]
fn completion_script_calls_back_into_mise() {
let script = Cli::completion_script(usage_rs::complete::Shell::Bash);
assert!(script.contains("mise' __complete_word__"), "{script}");
assert!(!script.contains("command usage"), "{script}");
}
}