97 lines
2.9 KiB
Rust
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);
|
|
}
|
|
}
|