use std::path::{Path, PathBuf};
use clap_complete::engine::CompletionCandidate;
use prost::Message as ProstMessage;
use prost_types::{FileDescriptorProto, FileDescriptorSet};
use crate::EMBEDDED_DESCRIPTOR;
pub fn flag_value_from_args(short: &str, long: &str) -> Option<std::ffi::OsString> {
let mut args = std::env::args_os().peekable();
for a in args.by_ref() {
if a == "--" {
break;
}
}
let mut found: Option<std::ffi::OsString> = None;
while let Some(a) = args.next() {
let s = a.to_string_lossy();
if s == long || s == short {
if let Some(val) = args.next() {
found = Some(val);
}
}
else if let Some(rest) = s.strip_prefix(&format!("{long}=")) {
found = Some(std::ffi::OsString::from(rest));
}
}
found
}
pub fn message_names_from_descriptor(bytes: &[u8]) -> Vec<String> {
fn collect(pkg_prefix: &str, messages: &[prost_types::DescriptorProto], out: &mut Vec<String>) {
for msg in messages {
let name = msg.name.as_deref().unwrap_or("");
let fqn = if pkg_prefix.is_empty() {
name.to_string()
} else {
format!("{pkg_prefix}.{name}")
};
out.push(fqn.clone());
collect(&fqn, &msg.nested_type, out);
}
}
let files: Vec<FileDescriptorProto> = if let Ok(fds) = FileDescriptorSet::decode(bytes) {
fds.file
} else if let Ok(fdp) = FileDescriptorProto::decode(bytes) {
vec![fdp]
} else {
return vec![];
};
let mut names = Vec::new();
for file in &files {
let pkg = file.package.as_deref().unwrap_or("");
collect(pkg, &file.message_type, &mut names);
}
names
}
fn complete_path_under(
incomplete: &std::ffi::OsStr,
base: &Path,
suffix_filter: Option<&str>,
dirs_only: bool,
) -> Vec<CompletionCandidate> {
let s = incomplete.to_string_lossy();
let incomplete_path = Path::new(incomplete);
let (typed_prefix, filename_stem) = if s.ends_with('/') {
(incomplete_path.to_path_buf(), String::new())
} else {
let parent = incomplete_path.parent().unwrap_or(Path::new(""));
let stem = incomplete_path
.file_name()
.unwrap_or(std::ffi::OsStr::new(""))
.to_string_lossy()
.into_owned();
(parent.to_path_buf(), stem)
};
let search_root = base.join(&typed_prefix);
let Ok(rd) = std::fs::read_dir(&search_root) else {
return vec![];
};
let mut completions: Vec<CompletionCandidate> = rd
.filter_map(|e| e.ok())
.filter(|e| e.file_name().to_string_lossy().starts_with(&filename_stem))
.filter_map(|e| {
let name = e.file_name();
let ft = e.file_type().ok()?;
if ft.is_dir() {
let p = typed_prefix.join(&name);
Some(CompletionCandidate::new(p.as_os_str().to_os_string()))
} else if ft.is_file() && !dirs_only {
let name_s = name.to_string_lossy();
if suffix_filter.is_none_or(|s| name_s.ends_with(s)) {
let p = typed_prefix.join(&name);
Some(CompletionCandidate::new(p.as_os_str().to_os_string()))
} else {
None
}
} else {
None
}
})
.collect();
completions.sort_by(|a, b| a.get_value().cmp(b.get_value()));
completions
}
pub fn complete_descriptor_path(incomplete: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
let base = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
complete_path_under(incomplete, &base, None, false)
}
pub fn complete_any_path(incomplete: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
let base = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
complete_path_under(incomplete, &base, None, false)
}
pub fn complete_dir_path(incomplete: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
let base = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
complete_path_under(incomplete, &base, None, true)
}
pub fn complete_type_names(incomplete: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
let bytes: std::borrow::Cow<[u8]> = if let Some(path) =
flag_value_from_args("", "--descriptor-set")
.or_else(|| flag_value_from_args("", "--descriptor"))
.or_else(|| std::env::var_os("PROTOTEXT_DEFAULT_DESCRIPTOR"))
{
match std::fs::read(&path) {
Ok(b) => std::borrow::Cow::Owned(b),
Err(_) => return vec![],
}
} else {
std::borrow::Cow::Borrowed(EMBEDDED_DESCRIPTOR)
};
let prefix = incomplete.to_string_lossy();
message_names_from_descriptor(&bytes)
.into_iter()
.filter(|name| name.starts_with(prefix.as_ref()))
.map(CompletionCandidate::new)
.collect()
}
pub fn complete_input_paths(incomplete: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
let base = flag_value_from_args("-I", "--input-root")
.map(PathBuf::from)
.filter(|p| p.is_dir())
.or_else(|| std::env::current_dir().ok())
.unwrap_or_else(|| PathBuf::from("."));
complete_path_under(incomplete, &base, None, false)
}