#![allow(
// slice::strip_suffix added in 1.51
clippy::manual_strip,
)]
mod args;
mod doc;
mod index;
mod parser;
mod source;
#[cfg(test)]
mod test_utils;
mod viewer;
use std::env;
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) = sources.find(&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<source::Sources> {
let mut vec = 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(source::Sources::new(vec))
}
fn get_default_sources() -> Vec<path::PathBuf> {
let mut default_sources = 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"));
let mut target_dir = get_target_dir();
target_dir.push("doc");
default_sources.push(target_dir);
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 get_target_dir() -> path::PathBuf {
env::var_os("CARGO_TARGET_DIR")
.or_else(|| env::var_os("CARGO_BUILD_TARGET_DIR"))
.map(From::from)
.unwrap_or_else(|| "./target".into())
}
fn search_doc(sources: &source::Sources, name: &doc::Name) -> anyhow::Result<Option<doc::Doc>> {
if let Some(item) = search_item(sources, name)? {
use anyhow::Context;
let doc = sources
.find(&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: &source::Sources,
name: &doc::Name,
) -> anyhow::Result<Option<index::IndexItem>> {
let items = sources.search(name)?;
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;
use std::str::FromStr;
anyhow::ensure!(
termion::is_tty(&io::stdin()),
"Found multiple matches for {}",
name
);
println!("Found multiple 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(input.trim()) {
Ok(items.get(i).map(Clone::clone))
} else {
Ok(None)
}
}
#[cfg(test)]
mod tests {
use crate::source;
use crate::test_utils::{with_rustdoc, Format};
#[test]
fn test_find_doc() {
with_rustdoc("*", Format::all(), |_, _, path| {
let sources = source::Sources::new(vec![source::get_source(path).unwrap()]);
assert!(sources
.find(&"kuchiki".to_owned().into(), None)
.unwrap()
.is_some());
assert!(sources
.find(&"kuchiki::NodeRef".to_owned().into(), None)
.unwrap()
.is_some());
assert!(sources
.find(&"kuchiki::NodeDataRef::as_node".to_owned().into(), None)
.unwrap()
.is_some());
assert!(sources
.find(&"kuchiki::traits".to_owned().into(), None)
.unwrap()
.is_some());
assert!(sources
.find(&"kachiki".to_owned().into(), None)
.unwrap()
.is_none());
});
}
}