syntext 1.1.1

Hybrid code search index for agent workflows
Documentation
//! Only-matching output renderer: prints only the matched portions of lines.

use std::collections::BTreeSet;
use std::io::{self, Write};
use std::path::Path;

use crate::path_util::path_bytes;
use crate::search::lines::for_each_line;
use crate::Config;

use super::{
    compile_output_regex, group_matches_by_path, read_repo_file_bytes, write_formatted_line,
};
use crate::cli::search::SearchArgs;

pub(in crate::cli) fn render_only_matching(
    config: &Config,
    matches: &[crate::SearchMatch],
    args: &SearchArgs,
) -> io::Result<()> {
    let re = compile_output_regex(args)?;
    if args.before_context > 0 || args.after_context > 0 {
        return render_only_matching_with_context(config, matches, args, &re);
    }
    let stdout = io::stdout();
    let mut out = stdout.lock();
    let unique_paths: BTreeSet<_> = matches.iter().map(|m| m.path.clone()).collect();
    let grouped_heading = args.heading && (unique_paths.len() > 1 || !args.no_filename);
    let suppress_path_prefix = args.heading;
    let mut current_path: Option<&Path> = None;

    for m in matches {
        let path_changed = current_path != Some(m.path.as_path());
        if path_changed && grouped_heading {
            if current_path.is_some() {
                writeln!(out)?;
            }
            if !args.no_filename {
                out.write_all(path_bytes(&m.path).as_ref())?;
                out.write_all(b"\n")?;
            }
        }
        current_path = Some(m.path.as_path());

        for matched in re.find_iter(&m.line_content) {
            if matched.start() == matched.end() {
                continue;
            }
            write_formatted_line(
                &mut out,
                suppress_path_prefix || args.no_filename,
                args.no_line_number,
                &m.path,
                m.line_number as usize,
                b':',
                &m.line_content[matched.start()..matched.end()],
            )?;
        }
    }

    Ok(())
}

fn render_only_matching_with_context(
    config: &Config,
    matches: &[crate::SearchMatch],
    args: &SearchArgs,
    re: &regex::bytes::Regex,
) -> io::Result<()> {
    let by_file = group_matches_by_path(matches);

    let stdout = io::stdout();
    let mut out = stdout.lock();
    let before = args.before_context;
    let after = args.after_context;
    let mut first_file = true;
    let grouped_heading = args.heading && (by_file.len() > 1 || !args.no_filename);
    let suppress_path_prefix = args.heading;

    for (rel_path, match_lines) in &by_file {
        let raw_content = match read_repo_file_bytes(config, rel_path) {
            Ok(b) => b,
            Err(_) => continue,
        };
        let file_content = crate::index::normalize_encoding(&raw_content, config.verbose);
        let mut file_lines: Vec<Vec<u8>> = Vec::new();
        for_each_line(file_content.as_ref(), |_, _, line| {
            file_lines.push(line.to_vec())
        });

        let match_set: BTreeSet<usize> = match_lines
            .iter()
            .map(|&n| (n as usize).saturating_sub(1))
            .collect();
        let mut to_print: BTreeSet<usize> = BTreeSet::new();
        for &mi in &match_set {
            let start = mi.saturating_sub(before);
            let end = (mi + after).min(file_lines.len().saturating_sub(1));
            for idx in start..=end {
                to_print.insert(idx);
            }
        }

        if !first_file && !to_print.is_empty() {
            if grouped_heading {
                writeln!(out)?;
            } else {
                writeln!(out, "{}", args.context_separator)?;
            }
        }
        if grouped_heading && !to_print.is_empty() && !args.no_filename {
            out.write_all(path_bytes(rel_path).as_ref())?;
            out.write_all(b"\n")?;
        }
        first_file = false;

        let mut prev: Option<usize> = None;
        for &idx in &to_print {
            if let Some(p) = prev {
                if idx > p + 1 {
                    writeln!(out, "{}", args.context_separator)?;
                }
            }

            let line_num = idx + 1;
            let content = file_lines.get(idx).map(Vec::as_slice).unwrap_or_default();
            if match_set.contains(&idx) {
                for matched in re.find_iter(content) {
                    if matched.start() == matched.end() {
                        continue;
                    }
                    write_formatted_line(
                        &mut out,
                        suppress_path_prefix || args.no_filename,
                        args.no_line_number,
                        rel_path,
                        line_num,
                        b':',
                        &content[matched.start()..matched.end()],
                    )?;
                }
            } else {
                write_formatted_line(
                    &mut out,
                    suppress_path_prefix || args.no_filename,
                    args.no_line_number,
                    rel_path,
                    line_num,
                    b'-',
                    content,
                )?;
            }

            prev = Some(idx);
        }
    }

    Ok(())
}