#![allow(clippy::match_like_matches_macro)]
mod args;
mod doc;
mod index;
mod parser;
mod source;
#[cfg(test)]
mod test_utils;
mod viewer;
use std::io;
use std::path;
fn main() -> anyhow::Result<()> {
env_logger::init();
let args = args::Args::load()?;
let sources = load_sources(&args.source_paths, !args.no_default_sources)?;
let doc = if let Some(doc) = find_doc(&sources, &args.keyword, None)? {
Some(doc)
} else if !args.no_search {
search_doc(&sources, &args.keyword)?
} else {
anyhow::bail!("Could not find documentation for {}", &args.keyword);
};
if let Some(doc) = doc {
let viewer = args.viewer.unwrap_or_else(viewer::get_default);
if args.examples {
let examples = doc.find_examples()?;
anyhow::ensure!(
!examples.is_empty(),
"Could not find examples for {}",
&args.keyword
);
viewer.open_examples(sources, args.viewer_args, &doc, examples)
} else {
viewer.open(sources, args.viewer_args, &doc)
}
} else {
Ok(())
}
}
fn load_sources(
sources: &[String],
load_default_sources: bool,
) -> anyhow::Result<Vec<Box<dyn source::Source>>> {
let mut vec: Vec<Box<dyn source::Source>> = Vec::new();
if load_default_sources {
for path in get_default_sources() {
if path.is_dir() {
vec.push(source::get_source(&path)?);
} else {
log::info!(
"Ignoring default source '{}' because it does not exist",
path.display()
);
}
}
}
for s in sources {
vec.push(source::get_source(s)?);
}
vec.reverse();
Ok(vec)
}
fn get_default_sources() -> Vec<path::PathBuf> {
let mut default_sources: Vec<path::PathBuf> = Vec::new();
let sysroot = get_sysroot().unwrap_or_else(|| path::PathBuf::from("/usr"));
default_sources.push(sysroot.join("share/doc/rust/html"));
default_sources.push(sysroot.join("share/doc/rust-doc/html"));
default_sources.push(path::PathBuf::from("./target/doc"));
default_sources
}
fn get_sysroot() -> Option<path::PathBuf> {
std::process::Command::new("rustc")
.arg("--print")
.arg("sysroot")
.output()
.ok()
.filter(|o| o.status.success())
.and_then(|o| String::from_utf8(o.stdout).ok())
.map(|s| s.trim().into())
}
fn find_doc(
sources: &[Box<dyn source::Source>],
name: &doc::Name,
ty: Option<doc::ItemType>,
) -> anyhow::Result<Option<doc::Doc>> {
let fqn = name.clone().into();
for source in sources {
if let Some(doc) = source.find_doc(&fqn, ty)? {
return Ok(Some(doc));
}
}
log::info!("Could not find item '{}'", fqn);
Ok(None)
}
fn search_doc(
sources: &[Box<dyn source::Source>],
name: &doc::Name,
) -> anyhow::Result<Option<doc::Doc>> {
if let Some(item) = search_item(sources, name)? {
use anyhow::Context;
let doc = find_doc(sources, &item.name, Some(item.ty))?
.with_context(|| format!("Could not find documentation for {}", &item.name))?;
Ok(Some(doc))
} else {
log::info!(
"Could not find documentation for '{}' in the search index",
name
);
Ok(None)
}
}
fn search_item(
sources: &[Box<dyn source::Source>],
name: &doc::Name,
) -> anyhow::Result<Option<index::IndexItem>> {
let indexes = sources
.iter()
.filter_map(|s| s.load_index().transpose())
.collect::<anyhow::Result<Vec<_>>>()?;
let mut items = indexes
.iter()
.map(|i| i.find(name))
.collect::<Vec<_>>()
.concat();
items.sort_unstable();
items.dedup();
if items.is_empty() {
Err(anyhow::anyhow!(
"Could not find documentation for {}",
&name
))
} else if items.len() == 1 {
log::info!("Search returned a single item: '{}'", &items[0].name);
Ok(Some(items[0].clone()))
} else {
select_item(&items, name)
}
}
fn select_item(
items: &[index::IndexItem],
name: &doc::Name,
) -> anyhow::Result<Option<index::IndexItem>> {
use std::io::Write;
anyhow::ensure!(
termion::is_tty(&io::stdin()),
"Found multiple matches for {}",
name
);
println!("Found mulitple matches for {} – select one of:", name);
println!();
let width = (items.len() + 1).to_string().len();
for (i, item) in items.iter().enumerate() {
println!("[ {:width$} ] {}", i, &item, width = width);
}
println!();
print!("> ");
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
if let Ok(i) = usize::from_str_radix(input.trim(), 10) {
Ok(items.get(i).map(Clone::clone))
} else {
Ok(None)
}
}
#[cfg(test)]
mod tests {
use crate::source;
use crate::test_utils::with_rustdoc;
#[test]
fn test_find_doc() {
with_rustdoc("*", |_, path| {
let sources = vec![source::get_source(path).unwrap()];
assert!(
super::find_doc(&sources, &"kuchiki".to_owned().into(), None)
.unwrap()
.is_some()
);
assert!(
super::find_doc(&sources, &"kuchiki::NodeRef".to_owned().into(), None)
.unwrap()
.is_some()
);
assert!(super::find_doc(
&sources,
&"kuchiki::NodeDataRef::as_node".to_owned().into(),
None
)
.unwrap()
.is_some());
assert!(
super::find_doc(&sources, &"kuchiki::traits".to_owned().into(), None)
.unwrap()
.is_some()
);
assert!(
super::find_doc(&sources, &"kachiki".to_owned().into(), None)
.unwrap()
.is_none()
);
});
}
}