~ireas/rusty-man

44b14055955c74828dfc3261034309d179a51c44 — Robin Krahl a month ago 2c1b914
Move item lookup logic into Sources struct

This match adds the Sources struct that is a wrapper for a vector of
Source structs.  It now contains the lookup logic that was previously
contained in functions in main.rs.
5 files changed, 90 insertions(+), 104 deletions(-)

M src/main.rs
M src/source.rs
M src/viewer/mod.rs
M src/viewer/text/mod.rs
M src/viewer/tui/mod.rs
M src/main.rs => src/main.rs +34 -74
@@ 9,9 9,8 @@
//!    and the `source` module.  Per default, we look for documentation in the directory
//!    `share/doc/rust{,-doc}/html` relative to the Rust installation path (`rustc --print sysroot`
//!    or `usr`) and in `./target/doc`.
//! 2. We try to look up the given keyword in all acailable sources, see the `find_doc` function
//!    and the `source` module for the lookup logic and the `doc` module for the loaded
//!    documentation.
//! 2. We try to look up the given keyword in all acailable sources, see the and the `source`
//!    module for the lookup logic and the `doc` module for the loaded documentation.
//! 3. If we didn’t find a match in the previous step, we load the search index from the
//!    `search-index.js` file for all sources and try to find a matching item.  If we find one, we
//!    open the documentation for that item as in step 2.  See the `search_doc` function and the


@@ 52,7 51,7 @@ fn main() -> anyhow::Result<()> {

    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)? {
    let doc = if let Some(doc) = sources.find(&args.keyword, None)? {
        Some(doc)
    } else if !args.no_search {
        search_doc(&sources, &args.keyword)?


@@ 80,11 79,8 @@ fn main() -> anyhow::Result<()> {
}

/// Load all sources given as a command-line argument and, if enabled, the default sources.
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();
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() {


@@ 106,7 102,7 @@ fn load_sources(
    // The last source should be searched first --> reverse source vector
    vec.reverse();

    Ok(vec)
    Ok(source::Sources::new(vec))
}

fn get_default_sources() -> Vec<path::PathBuf> {


@@ 132,32 128,14 @@ fn get_sysroot() -> Option<path::PathBuf> {
        .map(|s| s.trim().into())
}

/// Find the documentation for an item with the given name (exact matches only).
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)
}

/// Use the search index to find the documentation for an item that partially matches the given
/// keyword.
fn search_doc(
    sources: &[Box<dyn source::Source>],
    name: &doc::Name,
) -> anyhow::Result<Option<doc::Doc>> {
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 = find_doc(sources, &item.name, Some(item.ty))?
        let doc = sources
            .find(&item.name, Some(item.ty))?
            .with_context(|| format!("Could not find documentation for {}", &item.name))?;
        Ok(Some(doc))
    } else {


@@ 171,21 149,10 @@ fn search_doc(

/// Use the search index to find an item that partially matches the given keyword.
fn search_item(
    sources: &[Box<dyn source::Source>],
    sources: &source::Sources,
    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();

    let items = sources.search(name)?;
    if items.is_empty() {
        Err(anyhow::anyhow!(
            "Could not find documentation for {}",


@@ 213,7 180,7 @@ fn select_item(
        name
    );

    println!("Found mulitple matches for {} – select one of:", 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() {


@@ 240,35 207,28 @@ mod tests {
    #[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()
            );
            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());
        });
    }
}

M src/source.rs => src/source.rs +42 -0
@@ 22,6 22,9 @@ pub trait Source {
    fn load_index(&self) -> anyhow::Result<Option<index::Index>>;
}

/// A collection of sources.
pub struct Sources(Vec<Box<dyn Source>>);

/// Local directory containing documentation data.
///
/// The directory must contain documentation for one or more crates in subdirectories.  Suitable


@@ 32,6 35,45 @@ pub struct DirSource {
    path: path::PathBuf,
}

impl Sources {
    pub fn new(sources: Vec<Box<dyn Source>>) -> Sources {
        Sources(sources)
    }

    /// Find the documentation for an item with the given name (exact matches only).
    pub fn find(
        &self,
        name: &doc::Name,
        ty: Option<doc::ItemType>,
    ) -> anyhow::Result<Option<doc::Doc>> {
        let fqn = name.clone().into();
        for source in &self.0 {
            if let Some(doc) = source.find_doc(&fqn, ty)? {
                return Ok(Some(doc));
            }
        }
        log::info!("Could not find item '{}'", fqn);
        Ok(None)
    }

    /// Use the search index to find an item that partially matches the given keyword.
    pub fn search(&self, name: &doc::Name) -> anyhow::Result<Vec<index::IndexItem>> {
        let indexes = self
            .0
            .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();
        Ok(items)
    }
}

impl DirSource {
    fn new(path: path::PathBuf) -> Self {
        log::info!("Created directory source at '{}'", path.display());

M src/viewer/mod.rs => src/viewer/mod.rs +2 -2
@@ 15,14 15,14 @@ use crate::source;
pub trait Viewer: fmt::Debug {
    fn open(
        &self,
        sources: Vec<Box<dyn source::Source>>,
        sources: source::Sources,
        args: args::ViewerArgs,
        doc: &doc::Doc,
    ) -> anyhow::Result<()>;

    fn open_examples(
        &self,
        sources: Vec<Box<dyn source::Source>>,
        sources: source::Sources,
        args: args::ViewerArgs,
        doc: &doc::Doc,
        examples: Vec<doc::Example>,

M src/viewer/text/mod.rs => src/viewer/text/mod.rs +2 -2
@@ 45,7 45,7 @@ impl TextViewer {
impl viewer::Viewer for TextViewer {
    fn open(
        &self,
        _sources: Vec<Box<dyn source::Source>>,
        _sources: source::Sources,
        args: args::ViewerArgs,
        doc: &doc::Doc,
    ) -> anyhow::Result<()> {


@@ 54,7 54,7 @@ impl viewer::Viewer for TextViewer {

    fn open_examples(
        &self,
        _sources: Vec<Box<dyn source::Source>>,
        _sources: source::Sources,
        args: args::ViewerArgs,
        doc: &doc::Doc,
        examples: Vec<doc::Example>,

M src/viewer/tui/mod.rs => src/viewer/tui/mod.rs +10 -26
@@ 28,7 28,7 @@ impl TuiViewer {

    fn render<F>(
        &self,
        sources: Vec<Box<dyn source::Source>>,
        sources: source::Sources,
        args: args::ViewerArgs,
        doc: &doc::Doc,
        f: F,


@@ 49,7 49,7 @@ impl TuiViewer {
impl viewer::Viewer for TuiViewer {
    fn open(
        &self,
        sources: Vec<Box<dyn source::Source>>,
        sources: source::Sources,
        args: args::ViewerArgs,
        doc: &doc::Doc,
    ) -> anyhow::Result<()> {


@@ 58,7 58,7 @@ impl viewer::Viewer for TuiViewer {

    fn open_examples(
        &self,
        sources: Vec<Box<dyn source::Source>>,
        sources: source::Sources,
        args: args::ViewerArgs,
        doc: &doc::Doc,
        examples: Vec<doc::Example>,


@@ 70,16 70,13 @@ impl viewer::Viewer for TuiViewer {
}

pub struct Context {
    pub sources: Vec<Box<dyn source::Source>>,
    pub sources: source::Sources,
    pub args: args::ViewerArgs,
    pub highlighter: Option<utils::Highlighter>,
}

impl Context {
    pub fn new(
        sources: Vec<Box<dyn source::Source>>,
        args: args::ViewerArgs,
    ) -> anyhow::Result<Context> {
    pub fn new(sources: source::Sources, args: args::ViewerArgs) -> anyhow::Result<Context> {
        let highlighter = utils::get_highlighter(&args)?;
        Ok(Context {
            sources,


@@ 197,7 194,7 @@ fn create_backend() -> anyhow::Result<Box<dyn cursive::backend::Backend>> {
}

fn create_cursive(
    sources: Vec<Box<dyn source::Source>>,
    sources: source::Sources,
    args: args::ViewerArgs,
) -> anyhow::Result<cursive::Cursive> {
    use cursive::event::{Event, Key};


@@ 264,26 261,13 @@ fn handle_link(s: &mut cursive::Cursive, doc_name: &doc::Fqn, doc_ty: doc::ItemT
    }
}

fn find_doc(
    sources: &[Box<dyn source::Source>],
    ty: Option<doc::ItemType>,
    name: &doc::Fqn,
) -> anyhow::Result<doc::Doc> {
    for source in sources {
        if let Some(doc) = source.find_doc(name, ty)? {
            return Ok(doc);
        }
    }
    Err(anyhow::anyhow!(
        "Could not find documentation for item: {}",
        name
    ))
}

fn open_link(s: &mut cursive::Cursive, link: ResolvedLink) -> anyhow::Result<()> {
    match link {
        ResolvedLink::Doc(ty, name) => {
            let doc = find_doc(&context(s).sources, ty, &name)?;
            let doc = context(s)
                .sources
                .find(&name, ty)?
                .with_context(|| format!("Could not find documentation for item: {}", name))?;
            let mut renderer = context(s).create_renderer(&doc);
            renderer.render_doc(&doc).unwrap();
            let view = renderer.into_view();