//! `textDocument/foldingRange` provider. //! //! Strategy: one fold per heading (line → line before the next heading //! of same-or-higher level) plus one fold per top-level list block. //! Folding is deliberately conservative — we never produce nested folds //! beyond what the AST already groups, since vimwiki users mostly want //! "collapse this section" behaviour rather than fine-grained code-fold //! semantics. use nuwiki_core::ast::{BlockNode, DocumentNode}; use tower_lsp::lsp_types::{FoldingRange, FoldingRangeKind}; /// Compute folding ranges for the whole document. `total_lines` is the /// number of lines in the source (`text.lines().count() as u32`) — used /// to extend the last heading's fold to end-of-document. pub fn folding_ranges(ast: &DocumentNode, total_lines: u32) -> Vec { let mut out = Vec::new(); let last_line = total_lines.saturating_sub(1); // Collect heading spans + levels in source order. We walk the // top-level children only — nested headings inside blockquotes are // rare and folding them adds complexity for little gain. let mut headings: Vec<(u32, u8)> = Vec::new(); for block in &ast.children { if let BlockNode::Heading(h) = block { headings.push((h.span.start.line, h.level)); } } // For each heading, end-line is one before the next heading whose // level is <= current level (or last_line otherwise). for (i, &(line, level)) in headings.iter().enumerate() { let end = headings[i + 1..] .iter() .find(|(_, l)| *l <= level) .map(|(next_line, _)| next_line.saturating_sub(1)) .unwrap_or(last_line); if end > line { out.push(FoldingRange { start_line: line, end_line: end, start_character: None, end_character: None, kind: Some(FoldingRangeKind::Region), collapsed_text: None, }); } } // Fold each top-level list. Sublists fold implicitly because their // parent item's source span already covers them. for block in &ast.children { if let BlockNode::List(l) = block { let start = l.span.start.line; let end = l.span.end.line; if end > start { out.push(FoldingRange { start_line: start, end_line: end, start_character: None, end_character: None, kind: Some(FoldingRangeKind::Region), collapsed_text: None, }); } } } out } /// Count lines in `text` (always ≥ 1 for non-empty input). pub fn line_count(text: &str) -> u32 { if text.is_empty() { 0 } else { let mut n = text.bytes().filter(|b| *b == b'\n').count() as u32; if !text.ends_with('\n') { n += 1; } else { // trailing newline still bounds a virtual line; LSP positions // index 0-based, so the file ends at line `n` (the empty one // after the final newline). We treat "lines" as the count // *including* that trailing empty line for fold-end clamping. n += 1; } n } }