Files
rlogg/src/search.rs

184 lines
5.1 KiB
Rust

use rayon::prelude::*;
use regex::Regex;
use std::fs::File;
use std::io::BufReader;
use std::path::Path;
use std::sync::{Arc, Mutex};
use std::thread;
use crate::line_index::LineIndex;
use crate::types::FilteredLine;
pub const MAX_SEARCH_HISTORY: usize = 50;
pub struct SearchState {
pub searching: Arc<Mutex<bool>>,
pub progress: Arc<Mutex<f32>>,
pub results: Arc<Mutex<Option<Vec<FilteredLine>>>>,
}
impl SearchState {
pub fn new() -> Self {
Self {
searching: Arc::new(Mutex::new(false)),
progress: Arc::new(Mutex::new(0.0)),
results: Arc::new(Mutex::new(None)),
}
}
pub fn is_searching(&self) -> bool {
*self.searching.lock().unwrap()
}
pub fn get_progress(&self) -> f32 {
*self.progress.lock().unwrap()
}
pub fn take_results(&self) -> Option<Vec<FilteredLine>> {
self.results.lock().unwrap().take()
}
}
pub struct SearchParams {
pub query: String,
pub case_sensitive: bool,
pub use_regex: bool,
}
impl SearchParams {
fn build_regex_matcher(&self) -> Option<Regex> {
if !self.use_regex {
return None;
}
let pattern = if self.case_sensitive {
self.query.clone()
} else {
format!("(?i){}", self.query)
};
Regex::new(&pattern).ok()
}
fn matches_line(&self, content: &str, regex_matcher: &Option<Regex>) -> bool {
if let Some(regex) = regex_matcher {
regex.is_match(content)
} else if self.use_regex {
false
} else if self.case_sensitive {
content.contains(&self.query)
} else {
content.to_lowercase().contains(&self.query.to_lowercase())
}
}
}
pub fn start_search(
search_state: &SearchState,
params: SearchParams,
line_index: Arc<LineIndex>,
file_path: std::path::PathBuf,
) {
if search_state.is_searching() {
return;
}
let searching = Arc::clone(&search_state.searching);
let progress = Arc::clone(&search_state.progress);
let results = Arc::clone(&search_state.results);
*searching.lock().unwrap() = true;
*progress.lock().unwrap() = 0.0;
*results.lock().unwrap() = None;
thread::spawn(move || {
let filtered = search_lines(&params, &line_index, &file_path, &progress);
*results.lock().unwrap() = Some(filtered);
*progress.lock().unwrap() = 1.0;
*searching.lock().unwrap() = false;
});
}
fn search_lines(
params: &SearchParams,
line_index: &LineIndex,
file_path: &Path,
progress: &Arc<Mutex<f32>>,
) -> Vec<FilteredLine> {
let total_lines = line_index.total_lines;
if total_lines == 0 {
return Vec::new();
}
// Determine optimal chunk size based on total lines
// Aim for enough chunks to utilize all cores, but not too many to avoid overhead
let num_threads = rayon::current_num_threads();
let min_chunk_size = 1000; // Process at least 1000 lines per chunk
let chunk_size = (total_lines / (num_threads * 4)).max(min_chunk_size);
// Split line numbers into chunks
let chunks: Vec<(usize, usize)> = (0..total_lines)
.step_by(chunk_size)
.map(|start| {
let end = (start + chunk_size).min(total_lines);
(start, end)
})
.collect();
let total_chunks = chunks.len();
let processed_chunks = Arc::new(Mutex::new(0usize));
// Process chunks in parallel
let results: Vec<Vec<FilteredLine>> = chunks
.par_iter()
.filter_map(|(start, end)| {
// Each thread opens its own file handle
let file = File::open(file_path).ok()?;
let mut file_handle = BufReader::new(file);
let regex_matcher = params.build_regex_matcher();
let mut chunk_results = Vec::new();
// Read lines in this chunk efficiently (one seek, sequential reads)
let lines = line_index.read_line_range(&mut file_handle, *start, *end);
// Process each line - only store line numbers, not content
for (line_number, content) in lines {
if params.matches_line(&content, &regex_matcher) {
chunk_results.push(FilteredLine { line_number });
}
}
// Update progress
{
let mut count = processed_chunks.lock().unwrap();
*count += 1;
*progress.lock().unwrap() = *count as f32 / total_chunks as f32;
}
Some(chunk_results)
})
.collect();
// Flatten and sort results by line number
let mut filtered: Vec<FilteredLine> = results.into_iter().flatten().collect();
filtered.sort_by_key(|f| f.line_number);
filtered
}
pub fn add_to_history(history: &mut Vec<String>, query: &str) {
if query.is_empty() {
return;
}
if let Some(pos) = history.iter().position(|x| x == query) {
history.remove(pos);
}
history.insert(0, query.to_string());
if history.len() > MAX_SEARCH_HISTORY {
history.truncate(MAX_SEARCH_HISTORY);
}
}