use std::collections::HashSet;
use crate::text_buffer::TextBuffer;
use super::TAB_WIDTH;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FoldRegion {
pub start_line: usize,
pub end_line: usize,
}
impl FoldRegion {
pub fn new(start_line: usize, end_line: usize) -> Self {
Self { start_line, end_line }
}
}
fn indent_width(line: &str) -> Option<usize> {
let mut width = 0;
for c in line.chars() {
match c {
'\t' => width += TAB_WIDTH,
' ' => width += 1,
_ if c.is_whitespace() => width += 1,
_ => return Some(width),
}
}
None
}
pub fn compute_foldable_regions(buffer: &TextBuffer) -> Vec<FoldRegion> {
let line_count = buffer.line_count();
let indents: Vec<Option<usize>> =
(0..line_count).map(|i| indent_width(buffer.line(i))).collect();
let mut regions = Vec::new();
for i in 0..line_count {
let Some(header_indent) = indents[i] else {
continue; };
let mut last_content = i;
let mut j = i + 1;
while j < line_count {
match indents[j] {
None => j += 1, Some(indent) if indent > header_indent => {
last_content = j;
j += 1;
}
Some(_) => break, }
}
if last_content > i {
regions.push(FoldRegion::new(i, last_content));
}
}
regions
}
pub fn is_fold_header(regions: &[FoldRegion], line: usize) -> bool {
regions.iter().any(|r| r.start_line == line)
}
pub fn hidden_lines(
regions: &[FoldRegion],
collapsed: &HashSet<usize>,
) -> HashSet<usize> {
let mut hidden = HashSet::new();
for region in regions {
if collapsed.contains(®ion.start_line) {
hidden.extend((region.start_line + 1)..=region.end_line);
}
}
hidden
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_indent_width_blank_lines() {
assert_eq!(indent_width(""), None);
assert_eq!(indent_width(" "), None);
assert_eq!(indent_width("\t"), None);
}
#[test]
fn test_indent_width_expands_tabs() {
assert_eq!(indent_width("code"), Some(0));
assert_eq!(indent_width(" code"), Some(2));
assert_eq!(indent_width("\tcode"), Some(TAB_WIDTH));
assert_eq!(indent_width("\t code"), Some(TAB_WIDTH + 2));
}
#[test]
fn test_no_regions_for_flat_text() {
let buffer = TextBuffer::new("a\nb\nc");
assert!(compute_foldable_regions(&buffer).is_empty());
}
#[test]
fn test_simple_block() {
let buffer =
TextBuffer::new("fn main() {\n let x = 1;\n let y = 2;\n}");
let regions = compute_foldable_regions(&buffer);
assert_eq!(regions, vec![FoldRegion::new(0, 2)]);
}
#[test]
fn test_nested_blocks() {
let buffer = TextBuffer::new(
"outer:\n inner:\n deep\n deeper\n after_inner",
);
let regions = compute_foldable_regions(&buffer);
assert_eq!(regions, vec![FoldRegion::new(0, 4), FoldRegion::new(1, 3)]);
}
#[test]
fn test_blank_lines_inside_and_trailing() {
let buffer =
TextBuffer::new("def f():\n a = 1\n\n b = 2\n\ng = 3");
let regions = compute_foldable_regions(&buffer);
assert_eq!(regions, vec![FoldRegion::new(0, 3)]);
}
#[test]
fn test_hidden_lines_single_collapsed() {
let regions = vec![FoldRegion::new(0, 2)];
let collapsed: HashSet<usize> = [0].into_iter().collect();
let hidden = hidden_lines(®ions, &collapsed);
assert_eq!(hidden, [1, 2].into_iter().collect());
}
#[test]
fn test_hidden_lines_nested_union() {
let regions = vec![FoldRegion::new(0, 4), FoldRegion::new(1, 3)];
let collapsed: HashSet<usize> = [1].into_iter().collect();
assert_eq!(
hidden_lines(®ions, &collapsed),
[2, 3].into_iter().collect()
);
let collapsed: HashSet<usize> = [0, 1].into_iter().collect();
assert_eq!(
hidden_lines(®ions, &collapsed),
[1, 2, 3, 4].into_iter().collect()
);
}
#[test]
fn test_hidden_lines_ignores_unknown_collapsed() {
let regions = vec![FoldRegion::new(0, 2)];
let collapsed: HashSet<usize> = [99].into_iter().collect();
assert!(hidden_lines(®ions, &collapsed).is_empty());
}
#[test]
fn test_is_fold_header() {
let regions = vec![FoldRegion::new(0, 2), FoldRegion::new(5, 7)];
assert!(is_fold_header(®ions, 0));
assert!(is_fold_header(®ions, 5));
assert!(!is_fold_header(®ions, 1));
assert!(!is_fold_header(®ions, 3));
}
}