use crate::grep::result::{MatchLocation, SourceId};
#[derive(Debug, Clone)]
pub struct PositionTracker {
total_bytes: usize,
line_starts: Vec<usize>,
lines: Vec<String>,
current_line: String,
store_lines: bool,
}
impl Default for PositionTracker {
fn default() -> Self {
Self::new()
}
}
impl PositionTracker {
pub fn new() -> Self {
Self {
total_bytes: 0,
line_starts: vec![0], lines: Vec::new(),
current_line: String::new(),
store_lines: true,
}
}
pub fn without_line_storage() -> Self {
Self {
total_bytes: 0,
line_starts: vec![0],
lines: Vec::new(),
current_line: String::new(),
store_lines: false,
}
}
pub fn advance(&mut self, chunk: &str) {
for c in chunk.chars() {
let char_len = c.len_utf8();
self.total_bytes += char_len;
if c == '\n' {
self.line_starts.push(self.total_bytes);
if self.store_lines {
self.lines.push(std::mem::take(&mut self.current_line));
}
} else if self.store_lines {
self.current_line.push(c);
}
}
}
pub fn finish(&mut self) {
if self.store_lines && !self.current_line.is_empty() {
self.lines.push(std::mem::take(&mut self.current_line));
}
}
pub fn total_bytes(&self) -> usize {
self.total_bytes
}
pub fn line_count(&self) -> usize {
self.line_starts.len()
}
pub fn line_number(&self, byte_offset: usize) -> usize {
match self.line_starts.binary_search(&byte_offset) {
Ok(idx) => idx + 1, Err(idx) => idx, }
}
pub fn column(&self, byte_offset: usize) -> usize {
let line_num = self.line_number(byte_offset);
let line_start = self.line_starts.get(line_num - 1).copied().unwrap_or(0);
byte_offset - line_start + 1
}
pub fn get_line(&self, line_number: usize) -> Option<&str> {
if !self.store_lines {
return None;
}
self.lines
.get(line_number.saturating_sub(1))
.map(|s| s.as_str())
}
pub fn get_line_at(&self, byte_offset: usize) -> Option<&str> {
let line_num = self.line_number(byte_offset);
self.get_line(line_num)
}
pub fn locate(&self, byte_offset: usize, source: &SourceId) -> MatchLocation {
MatchLocation {
file_path: source.file_path.clone(),
archive_entry: source.archive_entry.clone(),
line_number: self.line_number(byte_offset),
column: self.column(byte_offset),
byte_offset,
}
}
pub fn line_byte_range(&self, line_number: usize) -> Option<(usize, usize)> {
if line_number == 0 || line_number > self.line_starts.len() {
return None;
}
let start = self.line_starts[line_number - 1];
let end = self
.line_starts
.get(line_number)
.copied()
.unwrap_or(self.total_bytes);
Some((start, end))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_single_line() {
let mut tracker = PositionTracker::new();
tracker.advance("Hello, World!");
tracker.finish();
assert_eq!(tracker.line_number(0), 1);
assert_eq!(tracker.column(0), 1);
assert_eq!(tracker.line_number(7), 1);
assert_eq!(tracker.column(7), 8);
assert_eq!(tracker.get_line(1), Some("Hello, World!"));
}
#[test]
fn test_multiple_lines() {
let mut tracker = PositionTracker::new();
tracker.advance("Hello\nWorld\nTest\n");
tracker.finish();
assert_eq!(tracker.line_number(0), 1);
assert_eq!(tracker.column(0), 1);
assert_eq!(tracker.line_number(4), 1);
assert_eq!(tracker.column(4), 5);
assert_eq!(tracker.line_number(6), 2);
assert_eq!(tracker.column(6), 1);
assert_eq!(tracker.line_number(12), 3);
assert_eq!(tracker.column(12), 1);
assert_eq!(tracker.get_line(1), Some("Hello"));
assert_eq!(tracker.get_line(2), Some("World"));
assert_eq!(tracker.get_line(3), Some("Test"));
}
#[test]
fn test_chunked_processing() {
let mut tracker = PositionTracker::new();
tracker.advance("Hello\n");
tracker.advance("World");
tracker.finish();
assert_eq!(tracker.line_number(0), 1);
assert_eq!(tracker.line_number(6), 2);
assert_eq!(tracker.get_line(1), Some("Hello"));
assert_eq!(tracker.get_line(2), Some("World"));
}
#[test]
fn test_empty_lines() {
let mut tracker = PositionTracker::new();
tracker.advance("A\n\nB\n");
tracker.finish();
assert_eq!(tracker.get_line(1), Some("A"));
assert_eq!(tracker.get_line(2), Some(""));
assert_eq!(tracker.get_line(3), Some("B"));
}
#[test]
fn test_line_byte_range() {
let mut tracker = PositionTracker::new();
tracker.advance("Hello\nWorld\n");
tracker.finish();
assert_eq!(tracker.line_byte_range(1), Some((0, 6))); assert_eq!(tracker.line_byte_range(2), Some((6, 12))); }
#[test]
fn test_without_line_storage() {
let mut tracker = PositionTracker::without_line_storage();
tracker.advance("Hello\nWorld\n");
tracker.finish();
assert_eq!(tracker.line_number(0), 1);
assert_eq!(tracker.line_number(6), 2);
assert!(tracker.get_line(1).is_none());
}
#[test]
fn test_unicode() {
let mut tracker = PositionTracker::new();
tracker.advance("héllo\nwörld\n");
tracker.finish();
assert_eq!(tracker.line_number(0), 1);
assert_eq!(tracker.line_number(7), 2); }
#[test]
fn test_get_line_at() {
let mut tracker = PositionTracker::new();
tracker.advance("Hello\nWorld\n");
tracker.finish();
assert_eq!(tracker.get_line_at(0), Some("Hello"));
assert_eq!(tracker.get_line_at(3), Some("Hello"));
assert_eq!(tracker.get_line_at(6), Some("World"));
assert_eq!(tracker.get_line_at(8), Some("World"));
}
}