sel-rs 0.2.2

Select slices from text files by line numbers, ranges, positions, or regex
Documentation
//! Plain-line formatter with optional line number, filename, and highlight.

use super::{FormatOpts, Formatter, ansi, push_lossy};
use crate::{Emit, Role};
use std::io::{self, Write};
use std::ops::Range;

pub struct PlainFormatter {
    pub opts: FormatOpts,
}

impl PlainFormatter {
    pub fn new(opts: FormatOpts) -> Self {
        Self { opts }
    }

    fn render_content(&self, bytes: &[u8], spans: &[Range<usize>]) -> String {
        if !self.opts.color || spans.is_empty() {
            return String::from_utf8_lossy(bytes).to_string();
        }
        let mut sorted = spans.to_vec();
        sorted.sort_by_key(|r| r.start);
        let mut out = String::new();
        let mut cursor = 0usize;
        for r in sorted {
            let s = r.start.min(bytes.len());
            let e = r.end.min(bytes.len());
            if s < cursor {
                continue;
            }
            push_lossy(&mut out, &bytes[cursor..s]);
            out.push_str(ansi::INVERSE);
            push_lossy(&mut out, &bytes[s..e]);
            out.push_str(ansi::RESET);
            cursor = e;
        }
        push_lossy(&mut out, &bytes[cursor..]);
        out
    }
}

impl Formatter for PlainFormatter {
    fn write(&mut self, sink: &mut dyn Write, emit: &Emit) -> io::Result<()> {
        self.opts.widen_for_line(emit.line.no);
        let marker = match emit.role {
            Role::Target if self.opts.target_marker => {
                ansi::paint(self.opts.color, ansi::GREEN, ">") + " "
            }
            _ => String::new(),
        };
        let prefix = self.opts.prefix(emit.line.no);
        let content = self.render_content(&emit.line.bytes, &emit.match_info.spans);
        writeln!(sink, "{marker}{prefix}{content}")
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{Line, MatchInfo};

    fn opts(color: bool) -> FormatOpts {
        FormatOpts {
            show_line_numbers: true,
            show_filename: false,
            filename: None,
            color,
            target_marker: true,
            line_number_width: 4,
        }
    }

    #[test]
    fn target_gets_marker_and_prefix() {
        let line = Line::new(7, b"hello".to_vec());
        let mi = MatchInfo {
            hit: true,
            ..Default::default()
        };
        let emit = Emit {
            line: &line,
            role: Role::Target,
            match_info: &mi,
        };
        let mut f = PlainFormatter::new(opts(false));
        let mut buf: Vec<u8> = Vec::new();
        f.write(&mut buf, &emit).unwrap();
        assert_eq!(String::from_utf8(buf).unwrap(), ">    7: hello\n");
    }

    #[test]
    fn context_has_no_marker() {
        let line = Line::new(3, b"ctx".to_vec());
        let mi = MatchInfo::default();
        let emit = Emit {
            line: &line,
            role: Role::Context,
            match_info: &mi,
        };
        let mut f = PlainFormatter::new(opts(false));
        let mut buf: Vec<u8> = Vec::new();
        f.write(&mut buf, &emit).unwrap();
        assert_eq!(String::from_utf8(buf).unwrap(), "   3: ctx\n");
    }

    #[test]
    fn filename_prefix_keeps_padded_line_number() {
        let line = Line::new(7, b"hello".to_vec());
        let mi = MatchInfo {
            hit: true,
            ..Default::default()
        };
        let emit = Emit {
            line: &line,
            role: Role::Target,
            match_info: &mi,
        };
        let mut opts = opts(false);
        opts.show_filename = true;
        opts.filename = Some("input.txt".to_string());
        let mut f = PlainFormatter::new(opts);
        let mut buf: Vec<u8> = Vec::new();
        f.write(&mut buf, &emit).unwrap();
        assert_eq!(String::from_utf8(buf).unwrap(), "> input.txt:   7: hello\n");
    }

    #[test]
    fn width_grows_for_large_line_numbers() {
        let line = Line::new(10000, b"wide".to_vec());
        let mi = MatchInfo {
            hit: true,
            ..Default::default()
        };
        let emit = Emit {
            line: &line,
            role: Role::Target,
            match_info: &mi,
        };
        let mut f = PlainFormatter::new(opts(false));
        let mut buf: Vec<u8> = Vec::new();
        f.write(&mut buf, &emit).unwrap();
        assert_eq!(String::from_utf8(buf).unwrap(), "> 10000: wide\n");
    }

    #[test]
    #[allow(clippy::single_range_in_vec_init)]
    fn spans_highlight_when_color_enabled() {
        let line = Line::new(1, b"an ERROR today".to_vec());
        let mi = MatchInfo {
            hit: true,
            spans: vec![3..8],
            ..Default::default()
        };
        let emit = Emit {
            line: &line,
            role: Role::Target,
            match_info: &mi,
        };
        let mut f = PlainFormatter::new(opts(true));
        let mut buf: Vec<u8> = Vec::new();
        f.write(&mut buf, &emit).unwrap();
        let s = String::from_utf8(buf).unwrap();
        assert!(s.contains("\x1b[7mERROR\x1b[0m"));
    }
}