#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct FoldRange {
pub header: usize,
pub last_hidden: usize,
}
impl FoldRange {
pub fn hidden_count(&self) -> usize {
self.last_hidden.saturating_sub(self.header)
}
pub fn hides(&self, line: usize) -> bool {
line > self.header && line <= self.last_hidden
}
}
#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)]
pub struct FoldState {
folds: Vec<FoldRange>,
}
impl FoldState {
pub fn is_hidden(&self, row: usize) -> bool {
self.folds.iter().any(|f| f.hides(row))
}
pub fn prune_invalid(&mut self, line_count: usize) {
self.folds.retain(|f| f.header < line_count);
}
pub fn is_folded_header(&self, row: usize) -> bool {
self.folds.iter().any(|f| f.header == row)
}
pub fn toggle(&mut self, header: usize, lines: &[String]) {
if let Some(pos) = self.folds.iter().position(|f| f.header == header) {
self.folds.remove(pos);
return;
}
if let Some(range) = detect_range(header, lines) {
self.folds
.retain(|f| !(f.header > range.header && f.header <= range.last_hidden));
let pos = self.folds.partition_point(|f| f.header < range.header);
self.folds.insert(pos, range);
}
}
#[allow(dead_code)]
pub fn logical_to_visual(&self, row: usize) -> Option<usize> {
if self.is_hidden(row) {
return None;
}
let mut visual = row;
for f in &self.folds {
if f.header >= row {
break;
}
visual -= f.hidden_count();
}
Some(visual)
}
#[allow(dead_code)]
pub fn visual_to_logical(&self, visual: usize) -> usize {
let mut logical = visual;
for f in &self.folds {
if f.header >= logical {
break;
}
logical += f.hidden_count();
}
logical
}
pub fn visible_lines<'a>(&self, lines: &'a [String]) -> Vec<(usize, &'a str)> {
lines
.iter()
.enumerate()
.filter(|(i, _)| !self.is_hidden(*i))
.map(|(i, s)| (i, s.as_str()))
.collect()
}
pub fn folds(&self) -> &[FoldRange] {
&self.folds
}
}
fn detect_range(header: usize, lines: &[String]) -> Option<FoldRange> {
let header_line = lines.get(header)?;
let trimmed = header_line.trim_end();
if trimmed.ends_with('{') || trimmed.ends_with('(') || trimmed.ends_with('[') {
brace_range(header, lines)
} else {
indent_range(header, lines)
}
}
fn brace_range(header: usize, lines: &[String]) -> Option<FoldRange> {
let open = lines[header].trim_end().chars().last()?;
let close = match open {
'{' => '}',
'(' => ')',
'[' => ']',
_ => return None,
};
let mut depth = 0i32;
for (i, line) in lines.iter().enumerate().skip(header) {
for ch in line.chars() {
if ch == open {
depth += 1;
}
if ch == close {
depth -= 1;
if depth == 0 && i > header {
return Some(FoldRange {
header,
last_hidden: i.saturating_sub(1),
});
}
}
}
}
None
}
fn indent_range(header: usize, lines: &[String]) -> Option<FoldRange> {
let header_indent = indent_level(&lines[header]);
let mut last = header;
for (i, line) in lines.iter().enumerate().skip(header + 1) {
if line.trim().is_empty() {
continue;
}
if indent_level(line) > header_indent {
last = i;
} else {
break;
}
}
if last == header {
return None;
}
Some(FoldRange {
header,
last_hidden: last,
})
}
fn indent_level(line: &str) -> usize {
line.chars().take_while(|c| *c == ' ').count()
+ line.chars().take_while(|c| *c == '\t').count() * 4
}
#[cfg(test)]
mod tests {
use super::*;
fn lines(text: &str) -> Vec<String> {
text.lines().map(|l| l.to_string()).collect()
}
#[test]
fn brace_fold_hides_inner_lines() {
let src = lines("fn foo() {\n let x = 1;\n x\n}");
let mut fs = FoldState::default();
fs.toggle(0, &src);
assert!(!fs.is_hidden(0)); assert!(fs.is_hidden(1));
assert!(fs.is_hidden(2));
assert!(!fs.is_hidden(3)); }
#[test]
fn toggle_twice_unfolds() {
let src = lines("fn foo() {\n x\n}");
let mut fs = FoldState::default();
fs.toggle(0, &src);
assert!(fs.is_hidden(1));
fs.toggle(0, &src);
assert!(!fs.is_hidden(1));
}
#[test]
fn logical_to_visual_skips_hidden() {
let src = lines("fn foo() {\n x\n y\n}");
let mut fs = FoldState::default();
fs.toggle(0, &src);
assert_eq!(fs.logical_to_visual(0), Some(0));
assert_eq!(fs.logical_to_visual(1), None);
assert_eq!(fs.logical_to_visual(3), Some(1));
}
#[test]
fn visual_to_logical_round_trips() {
let src = lines("fn foo() {\n x\n y\n}\nlet z = 1;");
let mut fs = FoldState::default();
fs.toggle(0, &src);
assert_eq!(fs.visual_to_logical(0), 0);
assert_eq!(fs.visual_to_logical(1), 3);
assert_eq!(fs.visual_to_logical(2), 4);
}
#[test]
fn indent_fold() {
let src = lines("class Foo:\n def bar(self):\n pass\n\nclass Bar:");
let mut fs = FoldState::default();
fs.toggle(0, &src);
assert!(fs.is_hidden(1));
assert!(fs.is_hidden(2));
assert!(!fs.is_hidden(4));
}
#[test]
fn prune_invalid_removes_folds_beyond_line_count() {
let src = lines("fn foo() {\n x\n}\nfn bar() {\n y\n}");
let mut fs = FoldState::default();
fs.toggle(0, &src);
fs.toggle(3, &src);
assert!(fs.is_hidden(1));
assert!(fs.is_hidden(4));
fs.prune_invalid(3);
assert!(fs.is_hidden(1), "fold at 0 should still exist and hide line 1");
assert!(!fs.is_hidden(4), "fold at 3 should be removed since line_count is 3");
}
}