use std::ops::Range; #[cfg(not(target_family = "wasm"))] use tree_sitter::Node; #[cfg(not(target_family = "wasm"))] pub use tree_sitter::Tree; #[cfg(target_family = "wasm")] /// Stub type for tree-sitter Tree on WASM (tree-sitter not available). pub struct Tree; #[cfg(not(target_family = "wasm"))] /// Minimum line span for a node to be considered foldable. const MIN_FOLD_LINES: usize = 2; /// A fold range representing a foldable code region. /// /// The fold range spans from start_line to end_line (inclusive). #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct FoldRange { /// Start line (inclusive) pub start_line: usize, /// End line (inclusive) pub end_line: usize, } impl FoldRange { pub fn new(start_line: usize, end_line: usize) -> Self { assert!( start_line <= end_line, "fold start_line must be <= end_line" ); Self { start_line, end_line, } } } #[cfg(not(target_family = "wasm"))] /// Check if a named node qualifies as a fold candidate. /// /// Uses a structural heuristic: any **named** node spanning ≥ MIN_FOLD_LINES /// is foldable. tree-sitter already parses code into semantic units (functions, /// classes, blocks, etc.), so named nodes naturally correspond to meaningful /// foldable regions across all languages without a per-language node-type list. fn is_foldable_node(node: &Node) -> bool { let start = node.start_position().row; let end = node.end_position().row; end.saturating_sub(start) >= MIN_FOLD_LINES } #[cfg(not(target_family = "wasm"))] /// Extract fold ranges only within a byte range (for incremental updates after edits). /// /// Skips subtrees entirely outside the range, making it O(nodes in range) /// instead of O(all nodes in tree). pub fn extract_fold_ranges_in_range(tree: &Tree, byte_range: Range) -> Vec { let mut ranges = Vec::new(); let root = tree.root_node(); let mut cursor = root.walk(); // Skip the root, it's not foldable. Use named_children to skip literal tokens. for child in root.named_children(&mut cursor) { collect_foldable_nodes_in_range(child, &byte_range, &mut ranges); } ranges.sort_by_key(|r| r.start_line); ranges.dedup_by_key(|r| r.start_line); ranges } #[cfg(not(target_family = "wasm"))] /// Recursively collect foldable nodes, skipping subtrees outside byte_range. fn collect_foldable_nodes_in_range( node: Node, byte_range: &Range, ranges: &mut Vec, ) { if node.end_byte() <= byte_range.start || node.start_byte() >= byte_range.end { return; } if !is_foldable_node(&node) { return; } ranges.push(FoldRange { start_line: node.start_position().row, end_line: node.end_position().row, }); let mut cursor = node.walk(); for child in node.named_children(&mut cursor) { collect_foldable_nodes_in_range(child, byte_range, ranges); } }