Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions docs/handbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -400,9 +400,10 @@ lla completion zsh --output ./_lla
lla completion zsh --path ~/.zsh/completions/_lla
```

Without either option, the command installs to its shell-specific default file.
`--path` overrides that installed file; `--output` writes the generated script
to the requested file. Release packages may also include pre-generated assets.
Without either option, the command prints the script to stdout, so it can be
redirected or evaluated by your shell. `--path` installs to the specified file;
`--output` writes the generated script to the requested file. Release packages
may also include pre-generated assets.

### Shortcuts

Expand Down
77 changes: 10 additions & 67 deletions lla/src/commands/command_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,12 @@ fn install_completion(
let mut buf = Vec::new();
clap_complete::generate(shell, app, env!("CARGO_PKG_NAME"), &mut buf);

match output_path {
Some(path) => {
match (output_path, custom_path) {
(None, None) => {
std::io::stdout().lock().write_all(&buf)?;
Ok(())
}
(Some(path), _) => {
if let Some(parent) = std::path::Path::new(path).parent() {
create_dir_all(parent)?;
}
Expand All @@ -72,75 +76,14 @@ fn install_completion(
}
Ok(())
}
None => {
let (install_path, post_install_msg) = if let Some(path) = custom_path {
(
std::path::PathBuf::from(path),
"Restart your shell to apply changes",
)
} else {
match shell {
clap_complete::Shell::Bash => {
let path = dirs::home_dir()
.map(|h| h.join(".local/share/bash-completion/completions"))
.ok_or_else(|| {
LlaError::Other("Could not determine home directory".into())
})?;
(
path.join("lla"),
"Restart your shell or run 'source ~/.bashrc'",
)
}
clap_complete::Shell::Fish => {
let path = dirs::home_dir()
.map(|h| h.join(".config/fish/completions"))
.ok_or_else(|| {
LlaError::Other("Could not determine home directory".into())
})?;
(
path.join("lla.fish"),
"Restart your shell or run 'source ~/.config/fish/config.fish'",
)
}
clap_complete::Shell::Zsh => {
let path = dirs::home_dir()
.map(|h| h.join(".zsh/completions"))
.ok_or_else(|| {
LlaError::Other("Could not determine home directory".into())
})?;
(
path.join("_lla"),
"Add 'fpath=(~/.zsh/completions $fpath)' to ~/.zshrc and restart your shell",
)
}
clap_complete::Shell::PowerShell => {
let path = dirs::home_dir()
.map(|h| h.join("Documents/WindowsPowerShell"))
.ok_or_else(|| {
LlaError::Other("Could not determine home directory".into())
})?;
(
path.join("lla.ps1"),
"Restart PowerShell or reload your profile",
)
}
clap_complete::Shell::Elvish => {
let path =
dirs::home_dir()
.map(|h| h.join(".elvish/lib"))
.ok_or_else(|| {
LlaError::Other("Could not determine home directory".into())
})?;
(path.join("lla.elv"), "Restart your shell")
}
_ => return Err(LlaError::Other(format!("Unsupported shell: {:?}", shell))),
}
};
(None, Some(path)) => {
let install_path = std::path::Path::new(path);
let post_install_msg = "Restart your shell to apply changes";

if let Some(parent) = install_path.parent() {
create_dir_all(parent)?;
}
fs::write(&install_path, buf)?;
fs::write(install_path, buf)?;

if color_state.is_enabled() {
println!(
Expand Down
68 changes: 68 additions & 0 deletions lla/tests/completions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
use std::fs;
use std::process::{Command, Output};
use tempfile::TempDir;

fn completion(home: &TempDir, shell: &str, options: &[&str]) -> Output {
Command::new(env!("CARGO_BIN_EXE_lla"))
.env("HOME", home.path())
.env("USERPROFILE", home.path())
.env("APPDATA", home.path().join("AppData/Roaming"))
.env("LOCALAPPDATA", home.path().join("AppData/Local"))
.env("XDG_CONFIG_HOME", home.path().join(".config"))
.env("NO_COLOR", "1")
.current_dir(home.path())
.args(["completion", shell])
.args(options)
.output()
.expect("run completion command")
}

#[test]
fn completions_default_to_stdout_without_installing_files() {
for shell in ["bash", "fish", "zsh", "powershell", "elvish"] {
let home = TempDir::new().unwrap();
let output = completion(&home, shell, &[]);
assert!(output.status.success(), "{shell}: {:?}", output);
let script = String::from_utf8(output.stdout).unwrap();
assert!(script.contains("lla"), "{shell}: missing completion script");
assert!(!script.contains("✓"), "{shell}: status message in script");
assert!(output.stderr.is_empty(), "{shell}: {:?}", output.stderr);
for path in [
".local/share/bash-completion/completions/lla",
".config/fish/completions/lla.fish",
".zsh/completions/_lla",
"Documents/WindowsPowerShell/lla.ps1",
".elvish/lib/lla.elv",
] {
assert!(!home.path().join(path).exists(), "{shell}: created {path}");
}
}
}

#[test]
fn explicit_paths_write_the_same_script_as_stdout() {
for shell in ["bash", "fish", "zsh", "powershell", "elvish"] {
let home = TempDir::new().unwrap();
let expected = completion(&home, shell, &[]);
assert!(expected.status.success());
for option in ["--output", "--path"] {
let path = home.path().join(format!("nested/{shell}/{option}"));
let output = completion(&home, shell, &[option, path.to_str().unwrap()]);
assert!(output.status.success(), "{shell} {option}: {:?}", output);
assert_eq!(fs::read(path).unwrap(), expected.stdout);
}
}
}

#[test]
fn output_takes_precedence_over_install_path() {
let home = TempDir::new().unwrap();
let output = completion(
&home,
"zsh",
&["--path", "install/_lla", "--output", "output/_lla"],
);
assert!(output.status.success(), "{:?}", output);
assert!(home.path().join("output/_lla").is_file());
assert!(!home.path().join("install").exists());
}
Loading