use std::io::{self, BufRead, BufReader};
use std::path::PathBuf;
use std::process::{Command, Stdio};
use crate::workspace;
pub fn cmd_expand(args: &[String]) {
if args.iter().any(|a| matches!(a.as_str(), "--help" | "-h")) {
print_usage();
return;
}
let Some(cwd) = workspace::current_dir().ok() else {
eprintln!("could not resolve current directory");
std::process::exit(1);
};
let project_root: PathBuf = workspace::find_project_root(&cwd).unwrap_or(cwd);
if !is_cargo_expand_installed() {
eprintln!("cargo-expand is not installed.");
eprintln!("run `cargo install cargo-expand` and re-run `hopper expand`,");
eprintln!("or pass `--install` to have hopper install it for you.");
if !args.iter().any(|a| a == "--install") {
std::process::exit(1);
}
if !install_cargo_expand() {
std::process::exit(1);
}
}
let mut passthrough: Vec<String> = Vec::with_capacity(args.len());
let mut filter: Option<String> = None;
let mut i = 0;
while i < args.len() {
match args[i].as_str() {
"--install" => {
}
"--filter" => {
i += 1;
filter = args.get(i).cloned();
}
other => passthrough.push(other.to_string()),
}
i += 1;
}
let want_default_features = !passthrough
.iter()
.any(|a| a == "--features" || a.starts_with("--features="));
let mut cmd = Command::new("cargo");
cmd.arg("expand");
cmd.current_dir(&project_root);
if want_default_features {
cmd.arg("--features").arg("proc-macros");
}
for a in &passthrough {
cmd.arg(a);
}
if let Some(substring) = filter {
cmd.stdout(Stdio::piped());
cmd.stderr(Stdio::inherit());
let mut child = match cmd.spawn() {
Ok(c) => c,
Err(e) => {
eprintln!("failed to spawn cargo expand: {e}");
std::process::exit(1);
}
};
let stdout = child.stdout.take().expect("cargo stdout piped above");
filter_stream(stdout, &substring);
let status = child.wait().expect("wait on cargo expand");
if !status.success() {
std::process::exit(status.code().unwrap_or(1));
}
} else {
cmd.stdout(Stdio::inherit());
cmd.stderr(Stdio::inherit());
let status = match cmd.status() {
Ok(s) => s,
Err(e) => {
eprintln!("failed to run cargo expand: {e}");
std::process::exit(1);
}
};
if !status.success() {
std::process::exit(status.code().unwrap_or(1));
}
}
}
fn print_usage() {
eprintln!("Usage: hopper expand [cargo-expand args...] [--filter <substring>]");
eprintln!();
eprintln!("Expand `#[hopper::*]` (and every other proc-macro) in the current");
eprintln!("crate and print the result. A thin Hopper-flavoured wrapper over");
eprintln!("`cargo expand` with sensible defaults.");
eprintln!();
eprintln!("Hopper-specific flags:");
eprintln!(" --filter <substring> Only emit items whose spelling contains");
eprintln!(" <substring>. Matches struct, fn, and impl");
eprintln!(" headers.");
eprintln!(" --install Install cargo-expand if it is missing.");
eprintln!();
eprintln!("All other flags forward to `cargo expand` unchanged.");
}
fn is_cargo_expand_installed() -> bool {
let Ok(output) = Command::new("cargo")
.arg("expand")
.arg("--version")
.output()
else {
return false;
};
output.status.success()
}
fn install_cargo_expand() -> bool {
eprintln!("running: cargo install cargo-expand");
let status = Command::new("cargo")
.arg("install")
.arg("cargo-expand")
.status();
match status {
Ok(s) if s.success() => true,
Ok(s) => {
eprintln!("cargo install cargo-expand exited {s}");
false
}
Err(e) => {
eprintln!("failed to spawn cargo install: {e}");
false
}
}
}
fn filter_stream<R: io::Read>(stream: R, substring: &str) {
let reader = BufReader::new(stream);
let mut buf: Vec<String> = Vec::new();
let mut depth: i32 = 0;
for line in reader.lines() {
let Ok(line) = line else { continue };
buf.push(line.clone());
for c in line.chars() {
if c == '{' {
depth += 1;
} else if c == '}' {
depth -= 1;
}
}
if depth <= 0 {
let chunk = buf.join("\n");
if chunk.contains(substring) {
println!("{chunk}");
}
buf.clear();
if depth < 0 {
depth = 0;
}
}
}
if !buf.is_empty() {
let chunk = buf.join("\n");
if chunk.contains(substring) {
println!("{chunk}");
}
}
}