Files
coop/crates/ui/src/input/display_map/folding.rs

97 lines
2.9 KiB
Rust

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<usize>) -> Vec<FoldRange> {
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<usize>,
ranges: &mut Vec<FoldRange>,
) {
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);
}
}