tgrep 1.6.15

Toy grep that honors .gitignore
Documentation
use std::fs;
use std::hint::black_box;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};

use criterion::{criterion_group, criterion_main, Criterion};
use regex::Regex;

use tgrep::utils::display::{Display, DisplayTerminal, Format, PathFormat};
use tgrep::utils::filters::Filters;
use tgrep::utils::grep::{self, Grep};
use tgrep::utils::lines::Zero;
use tgrep::utils::mapped::Mapped;
use tgrep::utils::matcher::{Match, Matcher, MatcherOptions};
use tgrep::utils::patterns::Patterns;
use tgrep::utils::walker::WalkerBuilder;
use tgrep::utils::writer::Writer;

fn unique_path(prefix: &str) -> PathBuf {
    let mut path = std::env::temp_dir();
    let unique = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap()
        .as_nanos();
    path.push(format!("{}-{}", prefix, unique));
    path
}

struct TempTree {
    path: PathBuf,
}

impl TempTree {
    fn new(prefix: &str) -> Self {
        let path = unique_path(prefix);
        fs::create_dir_all(&path).unwrap();
        Self { path }
    }

    fn path(&self) -> &Path {
        &self.path
    }

    fn write(&self, relative: &str, contents: &str) {
        let path = self.path.join(relative);
        if let Some(parent) = path.parent() {
            fs::create_dir_all(parent).unwrap();
        }
        fs::write(path, contents).unwrap();
    }

    fn collect_files(&self) -> Vec<PathBuf> {
        fn walk(path: &Path, files: &mut Vec<PathBuf>) {
            let entries = fs::read_dir(path).unwrap();
            for entry in entries {
                let entry = entry.unwrap();
                let path = entry.path();
                let file_type = entry.file_type().unwrap();
                if file_type.is_dir() {
                    walk(&path, files);
                } else if file_type.is_file() {
                    files.push(path);
                }
            }
        }

        let mut files = Vec::new();
        walk(&self.path, &mut files);
        files.sort();
        files
    }
}

impl Drop for TempTree {
    fn drop(&mut self) {
        let _ = fs::remove_dir_all(&self.path);
    }
}

#[derive(Clone, Default)]
struct NullWriter;

impl Writer for NullWriter {
    fn write(&self, _content: &str) {}
}

fn build_matcher(needle: &str) -> Matcher {
    let regexp = Regex::new(needle).unwrap();
    Arc::new(Box::new(move |line: &str, options| -> Option<Vec<Match>> {
        match options {
            MatcherOptions::Fuzzy => regexp
                .shortest_match(line)
                .map(|pos| vec![Match::new(0, pos)]),
            MatcherOptions::Exact(max) => {
                let mut matches = Vec::new();
                for (idx, hit) in regexp.find_iter(line).enumerate() {
                    matches.push(Match::new(hit.start(), hit.end()));
                    if idx + 1 == max {
                        break;
                    }
                }
                if matches.is_empty() {
                    None
                } else {
                    Some(matches)
                }
            }
        }
    }))
}

fn build_display(root: &Path) -> Arc<dyn Display> {
    let root = root.to_path_buf();
    let path_format: PathFormat = Arc::new(Box::new(move |path: &Path| {
        path.strip_prefix(&root)
            .unwrap()
            .to_string_lossy()
            .into_owned()
    }));
    Arc::new(DisplayTerminal::new(
        usize::MAX,
        Format::Rich {
            colour: false,
            match_only: false,
            no_path: false,
            no_lno: false,
        },
        path_format,
        Arc::new(NullWriter),
    ))
}

fn run_grep_on_files(files: &[PathBuf], matcher: Matcher, display: Arc<dyn Display>, grep: Grep) {
    for path in files {
        if let Some(mapped) = Mapped::open(path).unwrap() {
            if content_inspector::inspect(&mapped).is_binary() {
                continue;
            }
            grep(Arc::new(mapped), matcher.clone(), display.clone());
        } else {
            grep(
                Arc::new(Zero::new(path.to_path_buf())),
                matcher.clone(),
                display.clone(),
            );
        }
    }
}

fn bench_tree_sparse() -> TempTree {
    let tree = TempTree::new("tgrep-bench-sparse");
    tree.write(".gitignore", "ignored/\n");
    for dir_idx in 0..12 {
        for file_idx in 0..40 {
            let name = format!("dir-{dir_idx:02}/file-{file_idx:03}.txt");
            if file_idx % 17 == 0 {
                tree.write(&name, "alpha\nbeta\nneedle\ngamma\n");
            } else {
                tree.write(&name, "alpha\nbeta\ngamma\ndelta\n");
            }
        }
    }
    for file_idx in 0..80 {
        let name = format!("ignored/file-{file_idx:03}.txt");
        tree.write(&name, "needle\n");
    }
    tree
}

fn bench_tree_dense() -> TempTree {
    let tree = TempTree::new("tgrep-bench-dense");
    for dir_idx in 0..8 {
        for file_idx in 0..24 {
            let name = format!("dense-{dir_idx:02}/file-{file_idx:03}.txt");
            let mut body = String::new();
            for line_idx in 0..200 {
                let line = if line_idx % 3 == 0 {
                    "needle needle needle"
                } else {
                    "some ordinary text"
                };
                body.push_str(line);
                body.push('\n');
            }
            tree.write(&name, &body);
        }
    }
    tree
}

fn patterns_bench(c: &mut Criterion) {
    let _ = env_logger::builder().is_test(true).try_init();
    let patterns = Patterns::new("/", &["foo/bar/**/qux/xyz".to_string()]);
    c.bench_function("patterns_double_star_match", |b| {
        b.iter(|| {
            patterns.is_excluded(black_box("foo/bar/zoo/too/qux/xyz"), false);
        })
    });
}

fn traversal_bench(c: &mut Criterion) {
    let tree = bench_tree_sparse();
    let matcher = build_matcher("needle");
    let display = build_display(tree.path());
    let grep: Grep = Arc::new(Box::new(|_, _, _| {}));
    let file_filters = Filters::new(&["*.unlikely".to_string()]).unwrap();
    let ignore_patterns = Patterns::new(tree.path().to_str().unwrap(), &[]);
    let force_ignore_patterns = Patterns::new(tree.path().to_str().unwrap(), &[]);

    c.bench_function("walk_ignore_and_filter_only", |b| {
        b.iter(|| {
            let walker = WalkerBuilder::new(grep.clone(), matcher.clone(), display.clone())
                .ignore_patterns(ignore_patterns.clone())
                .force_ignore_patterns(force_ignore_patterns.clone())
                .file_filters(file_filters.clone())
                .build();
            walker.walk(black_box(tree.path()));
        })
    });
}

fn search_bench(c: &mut Criterion) {
    let tree = bench_tree_sparse();
    let files = tree.collect_files();
    let matcher = build_matcher("needle");
    let display = build_display(tree.path());
    let grep = grep::grep();

    c.bench_function("search_sorted_sparse_file_list", |b| {
        b.iter(|| {
            run_grep_on_files(
                black_box(&files),
                matcher.clone(),
                display.clone(),
                grep.clone(),
            );
        })
    });
}

fn end_to_end_bench(c: &mut Criterion) {
    let tree = bench_tree_dense();
    let matcher = build_matcher("needle");
    let display = build_display(tree.path());
    let grep = grep::grep();
    let file_filters = Filters::new(&["*".to_string()]).unwrap();
    let ignore_patterns = Patterns::new(tree.path().to_str().unwrap(), &[]);
    let force_ignore_patterns = Patterns::new(tree.path().to_str().unwrap(), &[]);

    c.bench_function("walk_and_search_dense_tree", |b| {
        b.iter(|| {
            let walker = WalkerBuilder::new(grep.clone(), matcher.clone(), display.clone())
                .ignore_patterns(ignore_patterns.clone())
                .force_ignore_patterns(force_ignore_patterns.clone())
                .file_filters(file_filters.clone())
                .build();
            walker.walk(black_box(tree.path()));
        })
    });
}

criterion_group!(
    benches,
    patterns_bench,
    traversal_bench,
    search_bench,
    end_to_end_bench
);
criterion_main!(benches);