thwack 0.10.7

Command line version of Go To File utility, similar to the one on GitHub
Documentation
use crate::Result;
use crate::matched_path::MatchedPath;
use crate::query::Query;
use crate::starting_point::StartingPoint;
use crate::tree::Tree;

#[derive(Debug)]
pub(crate) struct Candidates {
    paths: Vec<MatchedPath>,
    selected: Option<usize>,
}

impl Candidates {
    pub(crate) fn new(
        visible_paths_length: usize,
        starting_point: &StartingPoint,
        tree: &Tree,
        query: &Query,
    ) -> Result<Self> {
        let mut paths: Vec<MatchedPath> = Vec::new();
        for path in tree.iter() {
            match MatchedPath::new(&query.to_string(), starting_point.as_ref(), path) {
                Some(matched) => paths.push(matched),
                None => continue,
            }
        }
        paths.sort();
        paths.truncate(visible_paths_length);
        let selected = if paths.is_empty() { None } else { Some(0) };

        Ok(Self { paths, selected })
    }

    pub(crate) fn paths(&self) -> &[MatchedPath] {
        &self.paths
    }

    pub(crate) fn selected(&self) -> Option<&MatchedPath> {
        if let Some(s) = self.selected {
            return self.paths.get(s);
        }
        None
    }

    pub(crate) fn move_down(&mut self) {
        let limit = self.paths.len();
        if limit == 0 {
            return;
        }
        if let Some(s) = self.selected {
            if s < limit - 1 {
                self.selected = Some(s + 1);
            }
        } else {
            self.selected = Some(0);
        }
    }

    pub(crate) fn move_up(&mut self) {
        let limit = self.paths.len();
        if limit == 0 {
            return;
        }
        if let Some(s) = self.selected {
            if s > 0 {
                self.selected = Some(s - 1);
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::tree::tests::create_files;
    use git2::Repository;

    #[test]
    fn test_candidates_without_query() {
        let dir = create_files(true).unwrap();
        let starting_point = StartingPoint::new(dir.path()).unwrap();
        let query = Query::new("");
        let repo = Repository::open(dir.path()).unwrap();
        let tree = Tree::new(starting_point.as_ref(), Some(&repo)).unwrap();

        let candidates = Candidates::new(3, &starting_point, &tree, &query).unwrap();
        let result: Vec<String> = candidates
            .paths
            .iter()
            .map(|p| p.relative())
            .map(|m| format!("{}", m).replace('\\', "/"))
            .collect();
        assert_eq!(result, &[".browserslistrc", ".editorconfig", ".env"]);
    }

    #[test]
    fn test_candidates_with_query() {
        let dir = create_files(true).unwrap();
        let starting_point = StartingPoint::new(dir.path()).unwrap();
        let query = Query::new("bar");
        let repo = Repository::open(dir.path()).unwrap();
        let tree = Tree::new(starting_point.as_ref(), Some(&repo)).unwrap();
        let candidates = Candidates::new(5, &starting_point, &tree, &query).unwrap();
        let result: Vec<String> = candidates
            .paths
            .iter()
            .map(|p| p.relative())
            .map(|m| format!("{}", m).replace('\\', "/"))
            .collect();
        assert_eq!(result, &[".config/bar.toml", "lib/bar.js"]);
    }

    #[test]
    fn test_candidates_without_repo() {
        let dir = create_files(true).unwrap();
        let starting_point = StartingPoint::new(dir.path()).unwrap();
        let query = Query::new("");
        let tree = Tree::new(starting_point.as_ref(), None).unwrap();

        let candidates = Candidates::new(100, &starting_point, &tree, &query).unwrap();
        let result: Vec<String> = candidates
            .paths
            .iter()
            .map(|p| p.relative())
            .map(|m| format!("{}", m).replace('\\', "/"))
            .collect();
        assert!(result.contains(&"log.txt".to_string()));
        assert!(result.contains(&".git/config".to_string()));
    }

    #[test]
    fn test_move_down() {
        let dir = create_files(true).unwrap();
        let starting_point = StartingPoint::new(dir.path()).unwrap();
        let query = Query::new("");
        let repo = Repository::open(dir.path()).unwrap();
        let tree = Tree::new(starting_point.as_ref(), Some(&repo)).unwrap();

        let mut candidates = Candidates::new(3, &starting_point, &tree, &query).unwrap();
        assert_eq!(candidates.selected, Some(0));
        candidates.move_down();
        assert_eq!(candidates.selected, Some(1));
        candidates.move_down();
        assert_eq!(candidates.selected, Some(2));
        candidates.move_down();
        assert_eq!(candidates.selected, Some(2));
        candidates.move_down();
        assert_eq!(candidates.selected, Some(2));

        let mut candidates = Candidates::new(0, &starting_point, &tree, &query).unwrap();
        candidates.move_down();
        assert_eq!(candidates.selected, None);
        candidates.move_down();
        assert_eq!(candidates.selected, None);
    }

    #[test]
    fn test_move_up() {
        let dir = create_files(true).unwrap();
        let starting_point = StartingPoint::new(dir.path()).unwrap();
        let query = Query::new("");
        let repo = Repository::open(dir.path()).unwrap();
        let tree = Tree::new(starting_point.as_ref(), Some(&repo)).unwrap();

        let mut candidates = Candidates::new(3, &starting_point, &tree, &query).unwrap();
        assert_eq!(candidates.selected, Some(0));
        candidates.move_up();
        assert_eq!(candidates.selected, Some(0));
        candidates.move_down();
        candidates.move_down();
        candidates.move_down();
        assert_eq!(candidates.selected, Some(2));
        candidates.move_up();
        assert_eq!(candidates.selected, Some(1));
        candidates.move_up();
        assert_eq!(candidates.selected, Some(0));
        candidates.move_up();
        assert_eq!(candidates.selected, Some(0));

        let mut candidates = Candidates::new(0, &starting_point, &tree, &query).unwrap();
        candidates.move_up();
        assert_eq!(candidates.selected, None);
        candidates.move_up();
        assert_eq!(candidates.selected, None);
    }

    #[test]
    fn test_selected() {
        let dir = create_files(true).unwrap();
        let starting_point = StartingPoint::new(dir.path()).unwrap();
        let query = Query::new("");
        let repo = Repository::open(dir.path()).unwrap();
        let tree = Tree::new(starting_point.as_ref(), Some(&repo)).unwrap();

        let mut candidates = Candidates::new(3, &starting_point, &tree, &query).unwrap();
        assert_eq!(candidates.selected().unwrap().relative(), ".browserslistrc");

        candidates.move_down();
        assert!(
            candidates
                .selected()
                .unwrap()
                .relative()
                .ends_with(".editorconfig")
        );

        candidates.move_down();
        assert!(candidates.selected().unwrap().relative().ends_with(".env"));

        candidates.move_up();
        candidates.move_up();
        assert_eq!(candidates.selected().unwrap().relative(), ".browserslistrc");
    }

    #[test]
    fn test_selected_none_at_started() {
        let dir = create_files(false).unwrap();
        let starting_point = StartingPoint::new(dir.path()).unwrap();
        let query = Query::new("ABCABC!!!!!!!!!");
        let tree = Tree::new(starting_point.as_ref(), None).unwrap();

        let mut candidates = Candidates::new(3, &starting_point, &tree, &query).unwrap();
        assert_eq!(candidates.selected(), None);

        candidates.move_down();
        assert_eq!(candidates.selected(), None);

        candidates.move_down();
    }

    #[test]
    fn test_paths() {
        let dir = create_files(true).unwrap();
        let starting_point = StartingPoint::new(dir.path()).unwrap();
        let query = Query::new("");
        let repo = Repository::open(dir.path()).unwrap();
        let tree = Tree::new(starting_point.as_ref(), Some(&repo)).unwrap();

        let candidates = Candidates::new(3, &starting_point, &tree, &query).unwrap();
        let result: Vec<String> = candidates
            .paths()
            .iter()
            .map(|p| p.relative())
            .map(|m| format!("{}", m).replace('\\', "/"))
            .collect();
        assert_eq!(result, &[".browserslistrc", ".editorconfig", ".env"]);
    }
}