cargo-port 0.1.2

A TUI for inspecting and managing Rust projects
mod dispatch;
mod index;

pub(super) use dispatch::dispatch_finder_action;
pub(super) use dispatch::handle_finder_text_key;
pub(super) use dispatch::render_finder_pane_body;
#[cfg(test)]
use dispatch::search_finder;
pub(super) use index::FINDER_COLUMN_COUNT;
pub(super) use index::FinderItem;
#[cfg(test)]
use index::FinderKind;
pub(super) use index::build_finder_index;
#[cfg(test)]
use index::build_search_tokens;

#[cfg(test)]
mod tests {
    use std::path::PathBuf;

    use super::*;
    use crate::project::AbsolutePath;
    use crate::project::Cargo;
    use crate::project::ExampleGroup;
    use crate::project::Package;
    use crate::project::ProjectType;
    use crate::project::RootItem;
    use crate::project::RustInfo;
    use crate::project::RustProject;
    use crate::project::VendoredPackage;
    use crate::project::Visibility;
    use crate::project::Workspace;

    fn test_path(path: &str) -> AbsolutePath {
        let pb = if path == "~" {
            dirs::home_dir().unwrap_or_else(|| PathBuf::from(path))
        } else if let Some(rest) = path.strip_prefix("~/") {
            dirs::home_dir()
                .unwrap_or_else(|| PathBuf::from("/tmp"))
                .join(rest)
        } else {
            PathBuf::from(path)
        };
        AbsolutePath::from(pb)
    }

    #[test]
    fn build_finder_index_includes_vendored_projects() {
        let ws = Workspace {
            path: test_path("~/rust/hana"),
            name: Some("hana".to_string()),
            rust: RustInfo {
                vendored: vec![VendoredPackage {
                    path: test_path("~/rust/hana/crates/clay-layout"),
                    name: Some("clay-layout".to_string()),
                    ..VendoredPackage::default()
                }],
                ..RustInfo::default()
            },
            ..Workspace::default()
        };
        let list_items = crate::tui::project_list::ProjectList::new(vec![RootItem::Rust(
            RustProject::Workspace(ws),
        )]);
        let (items, _widths) = build_finder_index(&list_items);
        assert!(items.iter().any(|item| {
            item.project_path == test_path("~/rust/hana/crates/clay-layout")
                && item.display_name == "clay-layout (vendored)"
                && item.branch.is_empty()
        }));
    }

    #[test]
    fn finder_single_word_does_not_match_across_unrelated_tokens() {
        let item = FinderItem {
            display_name:  "clay-layout (vendored)".to_string(),
            search_tokens: build_search_tokens(&[
                "clay-layout (vendored)",
                "clay-layout",
                "clay-layout",
                "~/rust/bevy_diegetic/clay-layout",
                "vendored",
                FinderKind::Project.label(),
            ]),
            kind:          FinderKind::Project,
            project_path:  test_path("~/rust/bevy_diegetic/clay-layout"),
            target_name:   None,
            parent_label:  "clay-layout".to_string(),
            branch:        String::new(),
            dir:           "~/rust/bevy_diegetic/clay-layout".to_string(),
            pr_target:     None,
        };

        let (results, total) = search_finder(&[item], "android", 50);
        assert!(results.is_empty());
        assert_eq!(total, 0);
    }

    #[test]
    fn finder_single_word_matches_directory_token() {
        let item = FinderItem {
            display_name:  "raylib_renderer".to_string(),
            search_tokens: build_search_tokens(&[
                "raylib_renderer",
                "clay-layout",
                "~/rust/bevy_diegetic/clay-layout",
                "",
                FinderKind::Example.label(),
            ]),
            kind:          FinderKind::Example,
            project_path:  test_path("~/rust/bevy_diegetic/clay-layout"),
            target_name:   Some("raylib_renderer".to_string()),
            parent_label:  "clay-layout".to_string(),
            branch:        String::new(),
            dir:           "~/rust/bevy_diegetic/clay-layout".to_string(),
            pr_target:     None,
        };

        let (results, total) = search_finder(&[item], "diegetic", 50);
        assert_eq!(results, vec![0]);
        assert_eq!(total, 1);
    }

    #[test]
    fn finder_multi_word_matches_across_tokens() {
        let item = FinderItem {
            display_name:  "build-easefunction-graphs".to_string(),
            search_tokens: build_search_tokens(&[
                "build-easefunction-graphs",
                "build-easefunction-graphs",
                "~/rust/bevy/tools/build-easefunction-graphs",
                "fix/position-before-size-v0.19",
                FinderKind::Binary.label(),
            ]),
            kind:          FinderKind::Binary,
            project_path:  test_path("~/rust/bevy/tools/build-easefunction-graphs"),
            target_name:   Some("build-easefunction-graphs".to_string()),
            parent_label:  "build-easefunction-graphs".to_string(),
            branch:        "fix/position-before-size-v0.19".to_string(),
            dir:           "~/rust/bevy/tools/build-easefunction-graphs".to_string(),
            pr_target:     None,
        };

        let (results, total) = search_finder(&[item], "tools graphs", 50);
        assert_eq!(results, vec![0]);
        assert_eq!(total, 1);
    }

    #[test]
    fn finder_query_treats_slash_as_word_separator() {
        let item = FinderItem {
            display_name:  "bevy".to_string(),
            search_tokens: build_search_tokens(&[
                "bevy",
                "bevy",
                "~/rust/bevy",
                "main",
                FinderKind::Project.label(),
            ]),
            kind:          FinderKind::Project,
            project_path:  test_path("~/rust/bevy"),
            target_name:   None,
            parent_label:  "bevy".to_string(),
            branch:        "main".to_string(),
            dir:           "~/rust/bevy".to_string(),
            pr_target:     None,
        };
        let index = [item];
        let (with_slash, total_slash) = search_finder(&index, "rust/", 50);
        assert_eq!(with_slash, vec![0]);
        assert_eq!(total_slash, 1);
        let (with_path, total_path) = search_finder(&index, "rust/bevy", 50);
        assert_eq!(with_path, vec![0]);
        assert_eq!(total_path, 1);
    }

    #[test]
    fn build_finder_index_tokenizes_display_name_and_dir_segments() {
        let pkg = Package {
            path: test_path("~/rust/bevy/tools/build-easefunction-graphs"),
            name: Some("build-easefunction-graphs".to_string()),
            rust: RustInfo {
                cargo: Cargo {
                    types: vec![ProjectType::Binary],
                    examples: vec![ExampleGroup {
                        category: String::new(),
                        names:    vec!["raylib_renderer".to_string()],
                    }],
                    ..Cargo::default()
                },
                ..RustInfo::default()
            },
            ..Package::default()
        };

        let (items, _widths) =
            build_finder_index(&crate::tui::project_list::ProjectList::new(vec![
                RootItem::Rust(RustProject::Package(pkg)),
            ]));
        assert!(items.iter().any(|item| {
            item.display_name == "build-easefunction-graphs"
                && item.search_tokens.iter().any(|token| token == "tools")
                && item.search_tokens.iter().any(|token| token == "graphs")
        }));
    }

    #[test]
    fn build_finder_index_excludes_non_visible_roots() {
        let visible = Package {
            path: test_path("~/rust/here"),
            name: Some("here".to_string()),
            ..Package::default()
        };
        let mut deleted = Package {
            path: test_path("~/rust/gone"),
            name: Some("gone".to_string()),
            ..Package::default()
        };
        deleted.rust.info.visibility = Visibility::Deleted;

        let list = crate::tui::project_list::ProjectList::new(vec![
            RootItem::Rust(RustProject::Package(visible)),
            RootItem::Rust(RustProject::Package(deleted)),
        ]);
        let (items, _widths) = build_finder_index(&list);

        assert!(
            items
                .iter()
                .any(|item| item.project_path == test_path("~/rust/here"))
        );
        assert!(
            items
                .iter()
                .all(|item| item.project_path != test_path("~/rust/gone"))
        );
    }
}