use crate::models::Span;
pub const PREVIEW_MAX_BYTES: usize = 512;
const PREVIEW_MAX_LINES: usize = 7;
fn floor_char_boundary(s: &str, at: usize) -> usize {
if at >= s.len() {
return s.len();
}
let mut i = at;
while i > 0 && !s.is_char_boundary(i) {
i -= 1;
}
i
}
pub fn truncate_bytes(s: &str, max: usize) -> &str {
if s.len() <= max {
s
} else {
&s[..floor_char_boundary(s, max)]
}
}
pub fn extract_preview_at(source: &str, start_line_idx: usize) -> String {
let mut out = String::new();
for line in source.lines().skip(start_line_idx).take(PREVIEW_MAX_LINES) {
if !out.is_empty() {
if out.len() + 1 > PREVIEW_MAX_BYTES {
break;
}
out.push('\n');
}
let remaining = PREVIEW_MAX_BYTES - out.len();
if line.len() <= remaining {
out.push_str(line);
} else {
out.push_str(truncate_bytes(line, remaining));
break;
}
}
out
}
pub fn extract_preview(source: &str, span: &Span) -> String {
extract_preview_at(source, span.start_line.saturating_sub(1))
}
pub fn extract_preview_offset(source: &str, span: &Span, line_offset: usize) -> String {
extract_preview_at(
source,
span.start_line
.saturating_sub(1)
.saturating_sub(line_offset),
)
}
pub const LINE_PREVIEW_MAX_BYTES: usize = 512;
pub const EXPAND_MAX_BYTES: usize = 32 * 1024;
pub fn line_preview(line: &str, match_at: usize) -> String {
if line.len() <= LINE_PREVIEW_MAX_BYTES {
return line.to_string();
}
let budget = LINE_PREVIEW_MAX_BYTES.saturating_sub(2);
let half = budget / 2;
let raw_start = match_at.saturating_sub(half);
let start = {
let mut i = raw_start.min(line.len());
while i < line.len() && !line.is_char_boundary(i) {
i += 1;
}
i
};
let end = floor_char_boundary(line, (start + budget).min(line.len()));
let mut out = String::with_capacity(LINE_PREVIEW_MAX_BYTES);
if start > 0 {
out.push('…');
}
out.push_str(&line[start..end]);
if end < line.len() {
out.push('…');
}
out
}
pub fn expand_preview(body: &str) -> String {
if body.len() <= EXPAND_MAX_BYTES {
return body.to_string();
}
let mut out = truncate_bytes(body, EXPAND_MAX_BYTES).to_string();
out.push('…');
out
}
#[cfg(test)]
mod tests {
use super::*;
fn span(line: usize) -> Span {
Span {
start_line: line,
end_line: line,
}
}
#[test]
fn a_single_enormous_line_is_capped() {
let source = "x".repeat(2_000_000);
let preview = extract_preview(&source, &span(1));
assert!(
preview.len() <= PREVIEW_MAX_BYTES,
"got {} bytes from a 2 MB single line",
preview.len()
);
}
#[test]
fn every_symbol_on_one_line_stays_bounded() {
let source = "x".repeat(1_000_000);
let total: usize = (0..1000)
.map(|_| extract_preview(&source, &span(1)).len())
.sum();
assert!(
total <= 1000 * PREVIEW_MAX_BYTES,
"1000 previews totalled {total} bytes"
);
}
#[test]
fn normal_source_is_unchanged_and_shows_seven_lines() {
let source = (1..=20)
.map(|i| format!("line {i}"))
.collect::<Vec<_>>()
.join("\n");
let preview = extract_preview(&source, &span(3));
assert_eq!(
preview,
"line 3\nline 4\nline 5\nline 6\nline 7\nline 8\nline 9"
);
}
#[test]
fn a_short_file_returns_what_it_has() {
assert_eq!(extract_preview("only one line", &span(1)), "only one line");
assert_eq!(extract_preview("a\nb", &span(1)), "a\nb");
}
#[test]
fn a_start_line_past_the_end_returns_empty_instead_of_panicking() {
assert_eq!(extract_preview("a\nb\nc", &span(99)), "");
assert_eq!(extract_preview("", &span(1)), "");
assert_eq!(extract_preview("a", &span(0)), "a", "0 must not underflow");
}
#[test]
fn an_offset_larger_than_the_start_line_does_not_underflow() {
let source = "a\nb\nc";
assert_eq!(extract_preview_offset(source, &span(1), 10), "a\nb\nc");
}
#[test]
fn truncation_lands_on_a_character_boundary() {
for filler in ["日", "😀", "é"] {
let source = filler.repeat(500_000);
let preview = extract_preview(&source, &span(1));
assert!(preview.len() <= PREVIEW_MAX_BYTES);
assert!(!preview.is_empty(), "{filler} produced nothing");
assert_eq!(
String::from_utf8(preview.clone().into_bytes()).unwrap(),
preview
);
}
}
#[test]
fn a_long_line_among_normal_ones_is_capped_but_still_returned() {
let source = format!("fn a() {{}}\n{}\nfn b() {{}}", "z".repeat(100_000));
let preview = extract_preview(&source, &span(2));
assert!(preview.len() <= PREVIEW_MAX_BYTES);
assert!(preview.starts_with('z'));
}
#[test]
fn truncate_bytes_never_splits_a_character() {
assert_eq!(truncate_bytes("日本語", 4), "日");
assert_eq!(truncate_bytes("abc", 10), "abc");
assert_eq!(truncate_bytes("日", 1), "");
}
}
#[cfg(test)]
mod line_preview_tests {
use super::*;
#[test]
fn a_short_line_is_returned_whole() {
assert_eq!(line_preview("fn main() {}", 3), "fn main() {}");
}
#[test]
fn a_huge_line_is_capped() {
let line = "x".repeat(1_452_753);
let out = line_preview(&line, 0);
assert!(out.len() <= LINE_PREVIEW_MAX_BYTES + 4, "{}", out.len());
}
#[test]
fn the_window_is_centred_on_the_match() {
let line = format!("{}NEEDLE{}", "a".repeat(900_000), "b".repeat(500_000));
let out = line_preview(&line, 900_000);
assert!(out.contains("NEEDLE"), "match not in window: {out:.80}");
assert!(out.starts_with('…'), "elision not marked: {out:.20}");
assert!(out.ends_with('…'));
}
#[test]
fn a_match_near_the_start_has_no_leading_ellipsis() {
let line = format!("NEEDLE{}", "b".repeat(100_000));
let out = line_preview(&line, 0);
assert!(out.starts_with("NEEDLE"), "{out:.40}");
assert!(out.ends_with('…'));
}
#[test]
fn windowing_lands_on_character_boundaries() {
let line = "日".repeat(200_000);
let out = line_preview(&line, 300_000);
assert!(out.len() <= LINE_PREVIEW_MAX_BYTES + 4);
assert_eq!(String::from_utf8(out.clone().into_bytes()).unwrap(), out);
}
#[test]
fn expand_gets_a_larger_ceiling_than_a_preview() {
let body = "y".repeat(100_000);
let out = expand_preview(&body);
assert!(out.len() <= EXPAND_MAX_BYTES + 4);
assert!(out.len() > LINE_PREVIEW_MAX_BYTES, "expand must show more");
assert_eq!(expand_preview("fn a() {}"), "fn a() {}");
}
}