wsb 0.251.6311

Workspace tools for every project under $PRJ: git across submodules, template stamping, GitHub, and the workbench
Documentation
use crate::prelude::*;
use grep::regex::RegexMatcher;
use grep::searcher::{sinks::UTF8, SearcherBuilder};
use ignore::WalkBuilder;

#[derive(Debug, Clone)]
pub struct WorkspaceSearchMatch {
    pub path: PathBuf,
    pub line_number: u64,
    pub line: String,
}

pub fn discover_files_with_suffix(root: &Path, suffix: &str) -> Result<Vec<PathBuf>> {
    let mut paths = Vec::new();
    let walker = WalkBuilder::new(root)
        .hidden(false)
        .standard_filters(true)
        .build();

    for entry in walker {
        let entry = entry.with_context(|| {
            format!(
                "Failed to walk workspace while searching in {}",
                root.display()
            )
        })?;
        let path = entry.path();
        if entry
            .file_type()
            .map(|kind| kind.is_file())
            .unwrap_or(false)
            && path
                .file_name()
                .and_then(|name| name.to_str())
                .is_some_and(|name| name.ends_with(suffix))
        {
            paths.push(path.to_path_buf());
        }
    }

    paths.sort();
    paths.dedup();
    Ok(paths)
}

pub fn search_workspace(root: &Path, pattern: &str) -> Result<Vec<WorkspaceSearchMatch>> {
    let matcher = RegexMatcher::new(pattern)
        .with_context(|| format!("Invalid search pattern '{}'", pattern))?;
    let mut searcher = SearcherBuilder::new().line_number(true).build();
    let mut matches = Vec::new();

    let walker = WalkBuilder::new(root)
        .hidden(false)
        .standard_filters(true)
        .build();

    for entry in walker {
        let entry = entry.with_context(|| {
            format!(
                "Failed to walk workspace while searching in {}",
                root.display()
            )
        })?;
        let path = entry.path();
        if !entry
            .file_type()
            .map(|kind| kind.is_file())
            .unwrap_or(false)
        {
            continue;
        }

        let path_buf = path.to_path_buf();
        searcher
            .search_path(
                &matcher,
                path,
                UTF8(|line_number, line| {
                    matches.push(WorkspaceSearchMatch {
                        path: path_buf.clone(),
                        line_number,
                        line: line.to_string(),
                    });
                    Ok(true)
                }),
            )
            .with_context(|| format!("Failed to search {}", path.display()))?;
    }

    Ok(matches)
}