use std::ops::Range;
use super::{Error, Query};
use crate::patch::{DiffAttributes, FunctionLines};
const BINARY_PROBE: usize = 8000;
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
#[non_exhaustive]
pub enum Binary {
#[default]
Report,
Text,
Skip,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct Options {
invert: bool,
before: usize,
after: usize,
function_name: bool,
function_body: bool,
max_count: Option<usize>,
binary: Binary,
}
impl Options {
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_invert(mut self, invert: bool) -> Self {
self.invert = invert;
self
}
#[must_use]
pub fn with_before(mut self, lines: usize) -> Self {
self.before = lines;
self
}
#[must_use]
pub fn with_after(mut self, lines: usize) -> Self {
self.after = lines;
self
}
#[must_use]
pub fn with_function_name(mut self, function_name: bool) -> Self {
self.function_name = function_name;
self
}
#[must_use]
pub fn with_function_body(mut self, function_body: bool) -> Self {
self.function_body = function_body;
self
}
#[must_use]
pub fn with_max_count(mut self, max_count: Option<usize>) -> Self {
self.max_count = max_count;
self
}
#[must_use]
pub fn with_binary(mut self, binary: Binary) -> Self {
self.binary = binary;
self
}
pub fn invert(&self) -> bool {
self.invert
}
pub(super) fn has_context(&self) -> bool {
self.before > 0 || self.after > 0 || self.function_body
}
}
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
#[non_exhaustive]
pub enum LineKind {
Context,
Function,
Selected,
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub struct Line {
pub number: usize,
pub kind: LineKind,
pub range: Range<usize>,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
#[non_exhaustive]
pub struct FileMatches {
pub binary: bool,
pub count: usize,
pub lines: Vec<Line>,
}
impl FileMatches {
#[must_use]
pub fn is_match(&self) -> bool {
self.count > 0
}
}
#[derive(Debug)]
pub struct Searcher {
query: Query,
options: Options,
attributes: Option<DiffAttributes>,
}
impl Searcher {
pub fn new(query: Query, options: Options) -> Self {
Self {
query,
options,
attributes: None,
}
}
#[must_use]
pub fn with_attributes(mut self, attributes: DiffAttributes) -> Self {
self.attributes = Some(attributes);
self
}
pub fn query(&self) -> &Query {
&self.query
}
pub fn options(&self) -> &Options {
&self.options
}
pub fn search(&mut self, path: &[u8], content: &[u8]) -> Result<FileMatches, Error> {
let (binary, function) = match &mut self.attributes {
Some(attributes) if !path.is_empty() => {
attributes
.grep_driver(path)
.map_err(|source| Error::Attributes {
path: path.to_vec(),
source: Box::new(source),
})?
}
_ => (None, None),
};
let binary =
binary.unwrap_or_else(|| content.get(..BINARY_PROBE).unwrap_or(content).contains(&0));
let binary = match self.options.binary {
Binary::Text => false,
Binary::Skip if binary => return Ok(FileMatches::default()),
Binary::Report | Binary::Skip => binary,
};
let lines = split_lines(content);
let selected = self.select(content, &lines);
let count = selected.len();
if binary || count == 0 {
return Ok(FileMatches {
binary,
count,
lines: Vec::new(),
});
}
let shown = self.show(content, &lines, &selected, function.unwrap_or_default());
Ok(FileMatches {
binary,
count,
lines: shown,
})
}
fn select(&self, content: &[u8], lines: &[Range<usize>]) -> Vec<usize> {
let query = &self.query;
let options = &self.options;
let branches = query.all_match().then(|| query.branches());
let mut hit = vec![false; branches.as_ref().map_or(0, Vec::len)];
let mut selected = Vec::new();
let limit = options.max_count.unwrap_or(usize::MAX);
let skip = !options.invert && branches.is_none();
let mut index = 0;
while index < lines.len() {
let full = selected.len() >= limit;
if full && !hit.contains(&false) {
break;
}
if skip {
let Some(start) = lines
.get(index)
.and_then(|line| query.skip_to(content, line.start))
else {
break;
};
index += lines
.get(index..)
.unwrap_or_default()
.partition_point(|line| line.end < start);
}
let Some(range) = lines.get(index) else { break };
let line = content.get(range.clone()).unwrap_or_default();
let matched = match &branches {
Some(branches) => {
let mut any = false;
for (branch, hit) in branches.iter().zip(&mut hit) {
if query.eval(branch, line) {
*hit = true;
any = true;
}
}
any
}
None => query.is_match(line),
};
if matched != options.invert && !full {
selected.push(index);
}
index += 1;
}
if hit.contains(&false) {
selected.clear();
}
selected
}
fn show(
&self,
content: &[u8],
lines: &[Range<usize>],
selected: &[usize],
function: FunctionLines,
) -> Vec<Line> {
let options = &self.options;
let mut matcher = function.matcher();
let text = |index: usize| {
lines
.get(index)
.and_then(|range| content.get(range.clone()))
.unwrap_or_default()
};
let mut is_function = |index: usize| matcher.name(text(index)).is_some();
let is_blank = |index: usize| text(index).iter().all(u8::is_ascii_whitespace);
let mut kinds: Vec<Option<LineKind>> = vec![None; lines.len()];
let mark = |kinds: &mut Vec<Option<LineKind>>, index: usize, kind: LineKind| {
if let Some(slot) = kinds.get_mut(index) {
*slot = (*slot).max(Some(kind));
}
};
let mut last: Option<usize> = None;
for &index in selected {
if options.function_name
&& let Some(function) = (0..index).rev().find(|&line| is_function(line))
&& last.is_none_or(|last| function > last)
{
mark(&mut kinds, function, LineKind::Function);
}
let mut first = index.saturating_sub(options.before);
let mut end = index.saturating_add(options.after);
if options.function_body {
match (0..=index).rev().find(|&line| is_function(line)) {
Some(function) => {
mark(&mut kinds, function, LineKind::Function);
let mut start = function;
while start > 0 && !is_blank(start - 1) && !is_function(start - 1) {
start -= 1;
}
first = first.min(start);
}
None => first = 0,
}
let mut stop = (index + 1..lines.len())
.find(|&line| is_function(line))
.unwrap_or(lines.len());
while stop > index + 1 && is_blank(stop - 1) {
stop -= 1;
}
end = end.max(stop - 1);
}
let end = end.min(lines.len().saturating_sub(1));
for line in first..index {
mark(&mut kinds, line, LineKind::Context);
}
mark(&mut kinds, index, LineKind::Selected);
for line in index + 1..=end {
mark(&mut kinds, line, LineKind::Context);
}
last = last.max(Some(end));
}
kinds
.into_iter()
.zip(lines)
.enumerate()
.filter_map(|(index, (kind, range))| {
kind.map(|kind| Line {
number: index + 1,
kind,
range: range.clone(),
})
})
.collect()
}
}
fn split_lines(content: &[u8]) -> Vec<Range<usize>> {
let mut lines = Vec::new();
let mut start = 0;
for (index, &byte) in content.iter().enumerate() {
if byte == b'\n' {
lines.push(start..index);
start = index + 1;
}
}
if start < content.len() {
lines.push(start..content.len());
}
lines
}