use crate::error::{Result, SpliceError};
use std::path::Path;
use crate::output::SpanContext;
pub fn resolve_context_counts(
context_before: usize,
context_after: usize,
context_both: usize,
) -> (usize, usize) {
let before = context_before.max(context_both);
let after = context_after.max(context_both);
(before, after)
}
pub fn extract_context_asymmetric(
path: &Path,
byte_start: usize,
byte_end: usize,
context_before: usize,
context_after: usize,
) -> Result<SpanContext> {
use ropey::Rope;
if byte_start > byte_end {
return Err(SpliceError::InvalidSpan {
file: path.to_path_buf(),
start: byte_start,
end: byte_end,
file_size: 0, });
}
let contents = std::fs::read(path).map_err(|e| SpliceError::IoContext {
context: format!(
"Failed to read file for context extraction: {}",
path.display()
),
source: e,
})?;
let file_size = contents.len();
if byte_end > file_size {
return Err(SpliceError::InvalidSpan {
file: path.to_path_buf(),
start: byte_start,
end: byte_end,
file_size,
});
}
if file_size == 0 {
return Ok(SpanContext {
before: vec![],
selected: vec![],
after: vec![],
});
}
let rope =
Rope::from_str(
std::str::from_utf8(&contents).map_err(|e| SpliceError::InvalidUtf8 {
file: path.to_path_buf(),
source: e,
})?,
);
let start_line = rope.byte_to_line(byte_start);
let end_line = rope.byte_to_line(byte_end.saturating_sub(1));
let context_start = start_line.saturating_sub(context_before);
let context_end = (end_line + context_after + 1).min(rope.len_lines());
let before: Vec<String> = (context_start..start_line)
.map(|i| rope.line(i).to_string())
.collect();
let selected: Vec<String> = (start_line..=end_line)
.map(|i| rope.line(i).to_string())
.collect();
let after: Vec<String> = (end_line + 1..context_end)
.map(|i| rope.line(i).to_string())
.filter(|line| !line.is_empty())
.collect();
Ok(SpanContext {
before,
selected,
after,
})
}
pub fn extract_context_with_before_after(
path: &Path,
byte_start: usize,
byte_end: usize,
context_lines_before: usize,
context_lines_after: usize,
) -> Result<SpanContext> {
extract_context_asymmetric(
path,
byte_start,
byte_end,
context_lines_before,
context_lines_after,
)
}
pub fn extract_context(
path: &Path,
byte_start: usize,
byte_end: usize,
context_lines: usize,
) -> Result<SpanContext> {
extract_context_asymmetric(path, byte_start, byte_end, context_lines, context_lines)
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
#[test]
fn test_extract_context_basic() {
let mut file = NamedTempFile::new().unwrap();
writeln!(file, "line 1").unwrap();
writeln!(file, "line 2").unwrap();
writeln!(file, "line 3").unwrap();
writeln!(file, "line 4").unwrap();
writeln!(file, "line 5").unwrap();
let context = extract_context(file.path(), 7, 20, 1).unwrap();
assert_eq!(context.before.len(), 1); assert_eq!(context.selected.len(), 2); assert_eq!(context.after.len(), 1); }
#[test]
fn test_extract_context_zero_context() {
let mut file = NamedTempFile::new().unwrap();
writeln!(file, "line 1").unwrap();
writeln!(file, "line 2").unwrap();
writeln!(file, "line 3").unwrap();
let context = extract_context(file.path(), 7, 13, 0).unwrap();
assert_eq!(context.before.len(), 0);
assert_eq!(context.selected.len(), 1);
assert_eq!(context.after.len(), 0);
}
#[test]
fn test_extract_context_start_of_file() {
let mut file = NamedTempFile::new().unwrap();
writeln!(file, "line 1").unwrap();
writeln!(file, "line 2").unwrap();
writeln!(file, "line 3").unwrap();
let context = extract_context(file.path(), 0, 13, 2).unwrap();
assert_eq!(context.before.len(), 0); assert_eq!(context.selected.len(), 2);
assert_eq!(context.after.len(), 1); }
#[test]
fn test_extract_context_end_of_file() {
let mut file = NamedTempFile::new().unwrap();
writeln!(file, "line 1").unwrap();
writeln!(file, "line 2").unwrap();
writeln!(file, "line 3").unwrap();
let context = extract_context(file.path(), 14, 20, 2).unwrap();
assert_eq!(context.before.len(), 2); assert_eq!(context.selected.len(), 1); assert_eq!(context.after.len(), 0); }
#[test]
fn test_extract_context_utf8_multibyte() {
let mut file = NamedTempFile::new().unwrap();
writeln!(file, "line 🦀 1").unwrap();
writeln!(file, "line 🚀 2").unwrap();
writeln!(file, "line ⭐ 3").unwrap();
let contents = std::fs::read(file.path()).unwrap();
let rocket_line_start = contents.iter().position(|&b| b == b'2').unwrap();
let rocket_line_end = contents
.iter()
.skip(rocket_line_start)
.position(|&b| b == b'\n')
.unwrap()
+ rocket_line_start;
let context = extract_context(file.path(), rocket_line_start, rocket_line_end, 1).unwrap();
assert_eq!(context.selected.len(), 1);
assert!(context.selected[0].contains("🚀"));
}
#[test]
fn test_extract_context_invalid_span() {
let mut file = NamedTempFile::new().unwrap();
writeln!(file, "line 1").unwrap();
let result = extract_context(file.path(), 10, 5, 1);
assert!(result.is_err());
let result = extract_context(file.path(), 0, 1000, 1);
assert!(result.is_err());
}
#[test]
fn test_extract_context_empty_file() {
let file = NamedTempFile::new().unwrap();
let result = extract_context(file.path(), 0, 0, 1);
assert!(result.is_ok());
let context = result.unwrap();
assert_eq!(context.before.len(), 0);
assert_eq!(context.selected.len(), 0);
assert_eq!(context.after.len(), 0);
}
#[test]
fn test_extract_context_large_context_request() {
let mut file = NamedTempFile::new().unwrap();
writeln!(file, "line 1").unwrap();
writeln!(file, "line 2").unwrap();
writeln!(file, "line 3").unwrap();
let context = extract_context(file.path(), 7, 13, 100).unwrap();
assert_eq!(context.before.len(), 1); assert_eq!(context.selected.len(), 1);
assert_eq!(context.after.len(), 1); }
#[test]
fn test_extract_context_asymmetric_basic() {
let mut file = NamedTempFile::new().unwrap();
writeln!(file, "line 1").unwrap();
writeln!(file, "line 2").unwrap();
writeln!(file, "line 3").unwrap();
writeln!(file, "line 4").unwrap();
writeln!(file, "line 5").unwrap();
writeln!(file, "line 6").unwrap();
writeln!(file, "line 7").unwrap();
let context = extract_context_asymmetric(file.path(), 14, 28, 2, 1).unwrap();
assert_eq!(context.before.len(), 2); assert_eq!(context.selected.len(), 2); assert_eq!(context.after.len(), 1); }
#[test]
fn test_extract_context_asymmetric_zero_before() {
let mut file = NamedTempFile::new().unwrap();
writeln!(file, "line 1").unwrap();
writeln!(file, "line 2").unwrap();
writeln!(file, "line 3").unwrap();
writeln!(file, "line 4").unwrap();
let context = extract_context_asymmetric(file.path(), 7, 14, 0, 2).unwrap();
assert_eq!(context.before.len(), 0);
assert_eq!(context.selected.len(), 1);
assert_eq!(context.after.len(), 2); }
#[test]
fn test_extract_context_asymmetric_zero_after() {
let mut file = NamedTempFile::new().unwrap();
writeln!(file, "line 1").unwrap();
writeln!(file, "line 2").unwrap();
writeln!(file, "line 3").unwrap();
writeln!(file, "line 4").unwrap();
let context = extract_context_asymmetric(file.path(), 14, 21, 2, 0).unwrap();
assert_eq!(context.before.len(), 2); assert_eq!(context.selected.len(), 1);
assert_eq!(context.after.len(), 0);
}
}