pub const SNIPPETS_CONTEXT: i32 = 10;
pub const SNIPPETS_COLUMNS: usize = 500;
#[derive(Debug, Clone)]
pub struct SnippetViewport {
pub line: usize,
pub column: Option<usize>,
pub columns_per_line: usize,
pub line_context: i32,
pub wrap: bool,
pub show_line_numbers: bool,
pub highlight_line_number: bool,
}
impl SnippetViewport {
#[must_use]
pub const fn new(line: usize) -> Self {
Self {
line,
column: None,
columns_per_line: SNIPPETS_COLUMNS,
line_context: SNIPPETS_CONTEXT,
wrap: false,
show_line_numbers: true,
highlight_line_number: true,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct Function {
pub name: Option<String>,
pub node_type: Option<String>,
pub field_identifier_name: Option<String>,
}
#[derive(Debug, Clone)]
#[allow(clippy::struct_excessive_bools)]
pub struct Snippet {
pub content: String,
pub offset: usize,
pub line: Option<usize>,
pub column: Option<usize>,
pub columns_per_line: usize,
pub line_context: i32,
pub wrap: bool,
pub show_line_numbers: bool,
pub highlight_line_number: bool,
pub start_point: Option<(usize, usize)>,
pub end_point: Option<(usize, usize)>,
pub is_function: bool,
pub function: Option<Function>,
}
impl Default for Snippet {
fn default() -> Self {
Self {
content: String::new(),
offset: 0,
line: None,
column: None,
columns_per_line: SNIPPETS_COLUMNS,
line_context: SNIPPETS_CONTEXT,
wrap: false,
show_line_numbers: true,
highlight_line_number: true,
start_point: None,
end_point: None,
is_function: false,
function: None,
}
}
}
fn chunked_line(line: &str, chunk_size: usize) -> Vec<String> {
if line.is_empty() {
return vec![String::new()];
}
let chars: Vec<char> = line.chars().collect();
chars
.chunks(chunk_size)
.map(|chunk| chunk.iter().collect())
.collect()
}
fn get_lines(
lines_raw: &[String],
viewport: &SnippetViewport,
offset: usize,
) -> Vec<(usize, String)> {
if viewport.wrap {
let mut result = Vec::new();
for (i, line) in lines_raw.iter().enumerate() {
let line_no = i.saturating_add(1).saturating_add(offset);
for chunk in chunked_line(line, viewport.columns_per_line) {
result.push((line_no, chunk));
}
}
result
} else {
lines_raw
.iter()
.enumerate()
.map(|(i, line)| (i.saturating_add(1).saturating_add(offset), line.clone()))
.collect()
}
}
fn get_mark_symbol(line_no: usize, line_no_last: Option<usize>, viewport_line: usize) -> char {
if line_no == viewport_line && Some(line_no) != line_no_last {
'>'
} else {
' '
}
}
const fn must_highlight_line_number(viewport: &SnippetViewport) -> bool {
viewport.column.is_some() && viewport.highlight_line_number && viewport.show_line_numbers
}
fn format_line(
line: &str,
line_prefix: &str,
viewport: &SnippetViewport,
original_line: &str,
) -> String {
if !viewport.show_line_numbers {
let stripped_prefix = line_prefix.trim_end();
if original_line.starts_with(stripped_prefix) {
return line
.replacen(line_prefix, "", 1)
.replacen(stripped_prefix, "", 1);
}
return line.to_owned();
}
if !line.starts_with(line_prefix) && viewport.highlight_line_number {
let combined = format!("{line_prefix}{line}");
return combined.trim_end_matches(' ').to_owned();
}
let first_char = line_prefix.chars().next().unwrap_or_default();
if original_line.starts_with(first_char) {
return line.replacen(line_prefix, "", 1);
}
line.to_owned()
}
fn modify_lines(
lines: &mut [(usize, String)],
viewport: &SnippetViewport,
viewport_left: usize,
loc_width: usize,
) {
if lines.is_empty() {
return;
}
let mut line_no_last: Option<usize> = if lines.len() >= 2 {
lines.get(lines.len().saturating_sub(2)).map(|(n, _)| *n)
} else {
None
};
for index in 0..lines.len() {
let Some((line_no, line)) = lines.get(index).map(|(n, s)| (*n, s.clone())) else {
continue;
};
let mark_symbol = get_mark_symbol(line_no, line_no_last, viewport.line);
let line_no_str = if Some(line_no) == line_no_last {
String::new()
} else {
line_no.to_string()
};
line_no_last = Some(line_no);
let chars: Vec<char> = line.chars().collect();
let char_start = viewport_left.min(chars.len());
let char_end = viewport_left
.saturating_add(viewport.columns_per_line)
.saturating_add(1)
.min(chars.len());
let n_line: String = chars
.get(char_start..char_end)
.map(|s| s.iter().collect())
.unwrap_or_default();
let line_prefix = format!("{mark_symbol} {line_no_str:>loc_width$} | ");
let formatted = format_line(&n_line, &line_prefix, viewport, &line);
if let Some(entry) = lines.get_mut(index) {
*entry = (line_no, formatted);
}
}
}
#[allow(clippy::too_many_lines)]
pub fn make_snippet(
content: &str,
viewport: Option<&SnippetViewport>,
offset: Option<usize>,
) -> Snippet {
let lines_raw: Vec<String> = content
.replace('\t', " ")
.lines()
.map(std::borrow::ToOwned::to_owned)
.collect();
let mut offset = offset.unwrap_or(0);
let Some(vp) = viewport else {
return Snippet {
content: lines_raw.join("\n"),
offset,
..Snippet::default()
};
};
let mut lines = get_lines(&lines_raw, vp, offset);
let viewport_center = lines
.iter()
.position(|(line_no, _)| *line_no == vp.line)
.unwrap_or(0);
let viewport_left = vp.column.map_or(0, |col| {
col.saturating_sub(vp.columns_per_line.checked_div(4).unwrap_or(0))
});
if !lines.is_empty() {
let loc_width = lines.last().map_or(1, |(n, _)| n.to_string().len());
modify_lines(&mut lines, vp, viewport_left, loc_width);
if vp.line_context == -1 {
offset = 0;
} else {
let lc = i64::from(vp.line_context);
let vc = i64::try_from(viewport_center).unwrap_or(0);
let len = i64::try_from(lines.len()).unwrap_or(0);
if vc.saturating_sub(lc) <= 0 {
offset = 0;
let new_len =
usize::try_from(lc.saturating_mul(2).saturating_add(1)).unwrap_or(lines.len());
lines.truncate(new_len);
} else if vc.saturating_add(lc) < len {
let start = usize::try_from(vc.saturating_sub(lc).max(0)).unwrap_or(0);
let end =
usize::try_from(vc.saturating_add(lc).saturating_add(1)).unwrap_or(lines.len());
offset = start;
lines = lines
.get(start..end.min(lines.len()))
.map(<[(usize, String)]>::to_vec)
.unwrap_or_default();
} else {
let start = usize::try_from(
len.saturating_sub(lc.saturating_mul(2))
.saturating_sub(1)
.max(0),
)
.unwrap_or(0);
offset = start;
lines = lines
.get(start..)
.map(<[(usize, String)]>::to_vec)
.unwrap_or_default();
}
}
if must_highlight_line_number(vp) {
lines.push((0, format!(" {:>loc_width$} ^ Col {viewport_left}", " ")));
}
}
Snippet {
content: lines
.iter()
.map(|(_, s)| s.as_str())
.collect::<Vec<_>>()
.join("\n"),
offset,
line: Some(vp.line),
column: vp.column,
columns_per_line: vp.columns_per_line,
line_context: vp.line_context,
wrap: vp.wrap,
show_line_numbers: vp.show_line_numbers,
highlight_line_number: vp.highlight_line_number,
start_point: None,
end_point: None,
is_function: false,
function: None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_no_viewport_returns_all_lines() {
let snippet = make_snippet("alpha\nbeta\ngamma", None, None);
assert_eq!(snippet.content, "alpha\nbeta\ngamma");
assert_eq!(snippet.offset, 0);
assert!(snippet.line.is_none());
}
#[test]
fn test_tabs_replaced_with_spaces() {
let snippet = make_snippet("a\tb\tc", None, None);
assert_eq!(snippet.content, "a b c");
}
#[test]
fn test_focus_line_marked_with_arrow() {
let mut vp = SnippetViewport::new(3);
vp.line_context = 1;
let snippet = make_snippet("a\nb\nc\nd\ne", Some(&vp), None);
let arrow_on_focus = snippet
.content
.lines()
.any(|l| l.contains("| c") && l.starts_with('>'));
assert!(arrow_on_focus, "focus line must carry the '>' marker");
}
#[test]
fn test_non_focus_lines_not_marked() {
let mut vp = SnippetViewport::new(3);
vp.line_context = 1;
let snippet = make_snippet("a\nb\nc\nd\ne", Some(&vp), None);
let non_focus_has_arrow = snippet
.content
.lines()
.any(|l| !l.contains("| c") && l.starts_with('>'));
assert!(!non_focus_has_arrow, "only the focus line should carry '>'");
}
#[test]
fn test_window_near_start_zero_offset() {
let content = (1_usize..=20)
.map(|i| format!("line{i}"))
.collect::<Vec<_>>()
.join("\n");
let mut vp = SnippetViewport::new(2);
vp.line_context = 5;
let snippet = make_snippet(&content, Some(&vp), None);
assert_eq!(snippet.offset, 0);
assert_eq!(snippet.content.lines().count(), 11);
}
#[test]
fn test_window_centered_on_focus_line() {
let content = (1_usize..=20)
.map(|i| format!("line{i}"))
.collect::<Vec<_>>()
.join("\n");
let mut vp = SnippetViewport::new(10);
vp.line_context = 3;
let snippet = make_snippet(&content, Some(&vp), None);
assert_eq!(snippet.offset, 6);
assert_eq!(snippet.content.lines().count(), 7);
}
#[test]
fn test_window_near_end() {
let content = (1_usize..=20)
.map(|i| format!("line{i}"))
.collect::<Vec<_>>()
.join("\n");
let mut vp = SnippetViewport::new(19);
vp.line_context = 5;
let snippet = make_snippet(&content, Some(&vp), None);
assert_eq!(snippet.offset, 9);
assert_eq!(snippet.content.lines().count(), 11);
}
#[test]
fn test_line_context_minus_one_shows_all_lines() {
let content = "a\nb\nc\nd\ne";
let mut vp = SnippetViewport::new(3);
vp.line_context = -1;
let snippet = make_snippet(content, Some(&vp), None);
assert_eq!(snippet.offset, 0);
assert_eq!(snippet.content.lines().count(), 5);
}
}