use std::{
fs::File,
io::{BufRead, BufReader},
path::{Path, PathBuf},
thread,
};
use crossbeam_channel::Sender;
use eframe::egui;
use grep_regex::RegexMatcherBuilder;
use grep_searcher::{Searcher, Sink, SinkMatch};
use ignore::WalkBuilder;
#[derive(Debug, Clone)]
pub struct SearchMatch {
pub path: PathBuf,
pub line_number: u64,
pub line_content: String,
}
struct GuiSink {
path: PathBuf,
tx: Sender<SearchMatch>,
count: usize,
limit: usize,
}
impl Sink for GuiSink {
type Error = std::io::Error;
fn matched(&mut self, _searcher: &Searcher, mat: &SinkMatch<'_>) -> Result<bool, Self::Error> {
if self.count >= self.limit {
return Ok(false); }
let line_content = std::str::from_utf8(mat.bytes())
.unwrap_or("")
.trim_end()
.to_string();
let _ = self.tx.send(SearchMatch {
path: self.path.clone(),
line_number: mat.line_number().unwrap_or(0),
line_content,
});
self.count += 1;
Ok(true)
}
}
pub fn start_search(
query: String,
target_path: PathBuf,
ignore_case: bool,
read_hidden: bool,
limit: usize,
tx: Sender<SearchMatch>,
ctx: egui::Context,
) {
thread::spawn(move || {
let Ok(matcher) = RegexMatcherBuilder::new()
.case_insensitive(ignore_case)
.build(&query)
else {
return; };
let walker = WalkBuilder::new(target_path).hidden(!read_hidden).build();
let mut searcher = Searcher::new();
let mut total_matches = 0usize;
for result in walker {
if total_matches >= limit {
break; }
let Ok(entry) = result else {
continue;
};
if entry.file_type().is_some_and(|ft| ft.is_file()) {
let path = entry.path().to_path_buf();
let mut sink = GuiSink {
path: path.clone(),
tx: tx.clone(),
count: total_matches,
limit,
};
let _ = searcher.search_path(&matcher, &path, &mut sink);
total_matches = sink.count;
ctx.request_repaint();
}
}
});
}
pub fn read_line_range(
path: &Path,
start_line: usize,
end_line: usize,
) -> std::io::Result<Vec<String>> {
let file = File::open(path)?;
let reader = BufReader::new(file);
let mut lines = Vec::new();
for (idx, line) in reader.lines().enumerate() {
let line_num = idx + 1;
if line_num >= start_line && line_num <= end_line {
lines.push(line?);
}
if line_num > end_line {
break;
}
}
Ok(lines)
}
pub struct FilePreviewer {
pub path: PathBuf,
pub target_line: usize,
pub loaded_range: std::ops::Range<usize>, pub lines: Vec<String>,
pub error: Option<String>,
pub needs_scroll_to_target: bool,
pub current_scroll_offset: f32,
pub pending_scroll_adjustment: Option<f32>,
pub scroll_settled_delay: usize,
}
impl FilePreviewer {
#[must_use]
pub fn new(path: PathBuf, target_line: usize) -> Self {
let mut previewer = Self {
path,
target_line,
loaded_range: 0..0,
lines: Vec::new(),
error: None,
needs_scroll_to_target: true,
current_scroll_offset: 0.0,
pending_scroll_adjustment: None,
scroll_settled_delay: 5,
};
previewer.initial_load();
previewer
}
fn initial_load(&mut self) {
let start = if self.target_line > 40 {
self.target_line - 40
} else {
1
};
let end = self.target_line + 40;
match read_line_range(&self.path, start, end) {
Ok(lines) => {
self.lines = lines;
self.loaded_range = start..(start + self.lines.len());
}
Err(e) => {
self.error = Some(e.to_string());
}
}
}
pub fn load_more_above(&mut self, count: usize) {
if self.loaded_range.start <= 1 {
return;
}
let start = if self.loaded_range.start > count {
self.loaded_range.start - count
} else {
1
};
let end = self.loaded_range.start - 1;
match read_line_range(&self.path, start, end) {
Ok(new_lines) => {
let len = new_lines.len();
self.lines.splice(0..0, new_lines);
self.loaded_range = (self.loaded_range.start - len)..self.loaded_range.end;
#[allow(clippy::cast_precision_loss)]
let len_f = len as f32;
self.pending_scroll_adjustment = Some(len_f);
}
Err(e) => {
self.error = Some(e.to_string());
}
}
}
pub fn load_more_below(&mut self, count: usize) {
let start = self.loaded_range.end;
let end = start + count;
match read_line_range(&self.path, start, end) {
Ok(new_lines) => {
if new_lines.is_empty() {
return;
}
let len = new_lines.len();
self.lines.extend(new_lines);
self.loaded_range = self.loaded_range.start..(self.loaded_range.end + len);
}
Err(e) => {
self.error = Some(e.to_string());
}
}
}
}