Skip to main content

perl_lsp_folding/
lib.rs

1#![warn(missing_docs)]
2//! Folding range extraction for LSP textDocument/foldingRange
3//!
4//! This module provides folding range extraction from the Perl AST,
5//! allowing editors to collapse/expand code sections.
6
7use perl_lexer::{PerlLexer, TokenType};
8use perl_parser_core::ast::{Node, NodeKind, SourceLocation};
9
10/// Extracts folding ranges from a Perl AST
11pub struct FoldingRangeExtractor {
12    /// Accumulated folding ranges during extraction
13    ranges: Vec<FoldingRange>,
14}
15
16/// Represents a foldable region in the code for LSP folding range support.
17///
18/// Maps to LSP `FoldingRange` with byte offset coordinates for precise
19/// editor integration. Supports different fold types (comments, imports, regions)
20/// with optimal editor experience.
21///
22/// # Performance Characteristics
23/// - Memory footprint: 24 bytes per range (optimized for large files)
24/// - Range calculation: <1μs per fold region
25/// - LSP serialization: Direct mapping to protocol types
26#[derive(Debug, Clone)]
27pub struct FoldingRange {
28    /// Starting byte offset of the foldable region
29    pub start_offset: usize, // Changed from start_line to start_offset
30    /// Ending byte offset of the foldable region
31    pub end_offset: usize, // Changed from end_line to end_offset
32    /// Type of folding region for editor-specific handling
33    pub kind: Option<FoldingRangeKind>,
34}
35
36/// Classification of foldable regions for optimal editor experience.
37///
38/// Maps directly to LSP `FoldingRangeKind` enum with Perl-specific
39/// semantics for different code constructs.
40///
41/// # LSP Integration
42/// - `Comment`: Multi-line comments and POD documentation
43/// - `Imports`: `use` and `require` statement blocks
44/// - `Region`: Code blocks, subroutines, packages
45#[derive(Debug, Clone)]
46pub enum FoldingRangeKind {
47    /// Multi-line comments and POD documentation
48    Comment,
49    /// Use and require statement blocks
50    Imports,
51    /// Code blocks, subroutines, and packages
52    Region,
53}
54
55impl Default for FoldingRangeExtractor {
56    fn default() -> Self {
57        Self::new()
58    }
59}
60
61impl FoldingRangeExtractor {
62    /// Create a new folding range extractor
63    pub fn new() -> Self {
64        Self { ranges: Vec::new() }
65    }
66
67    /// Extract all folding ranges from the AST
68    pub fn extract(&mut self, ast: &Node) -> Vec<FoldingRange> {
69        self.ranges.clear();
70        self.visit_node(ast);
71        self.ranges.clone()
72    }
73
74    /// Extract heredoc folding ranges from source text using the lexer.
75    ///
76    /// Scans the source for heredoc bodies and returns their ranges.
77    pub fn extract_heredoc_ranges(text: &str) -> Vec<FoldingRange> {
78        let mut ranges = Vec::new();
79        let mut lexer = PerlLexer::new(text);
80
81        while let Some(token) = lexer.next_token() {
82            if matches!(token.token_type, TokenType::HeredocBody(_)) {
83                ranges.push(FoldingRange {
84                    start_offset: token.start,
85                    end_offset: token.end,
86                    kind: Some(FoldingRangeKind::Region),
87                });
88            }
89
90            // Stop at EOF
91            if matches!(token.token_type, TokenType::EOF) {
92                break;
93            }
94        }
95
96        ranges
97    }
98
99    /// Visit a node and extract folding ranges
100    fn visit_node(&mut self, node: &Node) {
101        match &node.kind {
102            NodeKind::Program { statements } => {
103                // Group consecutive use/require statements
104                let mut import_start: Option<usize> = None;
105                let mut import_end: Option<usize> = None;
106
107                for (i, stmt) in statements.iter().enumerate() {
108                    match &stmt.kind {
109                        NodeKind::Use { .. } | NodeKind::No { .. } => {
110                            if import_start.is_none() {
111                                import_start = Some(i);
112                            }
113                            import_end = Some(i);
114                        }
115                        _ => {
116                            // End of import block
117                            if let (Some(start_idx), Some(end_idx)) = (import_start, import_end) {
118                                if end_idx > start_idx {
119                                    // Multiple imports - create folding range
120                                    let start_loc = &statements[start_idx].location;
121                                    let end_loc = &statements[end_idx].location;
122                                    self.add_range_from_locations(
123                                        start_loc,
124                                        end_loc,
125                                        Some(FoldingRangeKind::Imports),
126                                    );
127                                }
128                            }
129                            import_start = None;
130                            import_end = None;
131                        }
132                    }
133
134                    // Visit each statement
135                    self.visit_node(stmt);
136                }
137
138                // Handle trailing imports
139                if let (Some(start_idx), Some(end_idx)) = (import_start, import_end) {
140                    if end_idx > start_idx {
141                        let start_loc = &statements[start_idx].location;
142                        let end_loc = &statements[end_idx].location;
143                        self.add_range_from_locations(
144                            start_loc,
145                            end_loc,
146                            Some(FoldingRangeKind::Imports),
147                        );
148                    }
149                }
150            }
151
152            NodeKind::Package { name: _, block, name_span: _ } => {
153                // Package with block is foldable
154                if let Some(block_node) = block {
155                    self.add_range_from_node(node, None);
156                    self.visit_node(block_node);
157                } else {
158                    // Even packages without explicit blocks could be foldable
159                    // if they span multiple lines (e.g., package Foo; ... package Bar;)
160                    self.add_range_from_node(node, None);
161                }
162            }
163
164            NodeKind::Subroutine { name: _, prototype: _, signature: _, body, .. }
165            | NodeKind::Method { name: _, signature: _, body, .. } => {
166                // Subroutines and methods are foldable
167                self.add_range_from_node(node, None);
168                self.visit_node(body);
169            }
170
171            NodeKind::Block { statements } => {
172                // Blocks are foldable if they contain statements
173                if !statements.is_empty() {
174                    self.add_range_from_node(node, None);
175                }
176                for stmt in statements {
177                    self.visit_node(stmt);
178                }
179            }
180
181            NodeKind::If { condition: _, then_branch, elsif_branches, else_branch } => {
182                // If statements with blocks are foldable
183                self.add_range_from_node(node, None);
184                self.visit_node(then_branch);
185                for (_, branch) in elsif_branches {
186                    self.visit_node(branch);
187                }
188                if let Some(else_br) = else_branch {
189                    self.visit_node(else_br);
190                }
191            }
192
193            NodeKind::While { condition: _, body, continue_block } => {
194                self.add_range_from_node(node, None);
195                self.visit_node(body);
196                if let Some(cont) = continue_block {
197                    self.visit_node(cont);
198                }
199            }
200
201            NodeKind::For { init: _, condition: _, update: _, body, continue_block: _ }
202            | NodeKind::Foreach { variable: _, list: _, body, continue_block: _ } => {
203                self.add_range_from_node(node, None);
204                self.visit_node(body);
205            }
206
207            NodeKind::Do { block } | NodeKind::Eval { block } => {
208                self.add_range_from_node(node, None);
209                self.visit_node(block);
210            }
211
212            NodeKind::Try { body, catch_blocks, finally_block } => {
213                self.add_range_from_node(node, None);
214                self.visit_node(body);
215                for (_, catch_block) in catch_blocks {
216                    self.visit_node(catch_block);
217                }
218                if let Some(finally) = finally_block {
219                    self.visit_node(finally);
220                }
221            }
222
223            NodeKind::Given { expr: _, body } => {
224                self.add_range_from_node(node, None);
225                self.visit_node(body);
226            }
227
228            NodeKind::PhaseBlock { phase: _, phase_span: _, block } => {
229                // BEGIN, END, CHECK, INIT blocks
230                self.add_range_from_node(node, None);
231                self.visit_node(block);
232            }
233
234            NodeKind::Class { name: _, body } => {
235                self.add_range_from_node(node, None);
236                self.visit_node(body);
237            }
238
239            // POD is typically inside strings or special constructs, not a separate NodeKind
240            NodeKind::Heredoc { .. } => {
241                // Heredocs are always foldable as regions
242                self.add_range_from_node(node, Some(FoldingRangeKind::Region));
243            }
244
245            NodeKind::StatementModifier { statement, modifier: _, condition } => {
246                self.visit_node(statement);
247                self.visit_node(condition);
248            }
249
250            NodeKind::ArrayLiteral { elements } => {
251                // Arrays are foldable if they have elements
252                // (They'll be filtered out later if too small)
253                if !elements.is_empty() {
254                    self.add_range_from_node(node, None);
255                }
256                for elem in elements {
257                    self.visit_node(elem);
258                }
259            }
260
261            NodeKind::HashLiteral { pairs } => {
262                // Hashes with elements are foldable
263                if !pairs.is_empty() {
264                    self.add_range_from_node(node, None);
265                }
266                for (key, value) in pairs {
267                    self.visit_node(key);
268                    self.visit_node(value);
269                }
270            }
271
272            // ArrayRef and HashRef don't exist as separate NodeKinds, they're handled via references
273            NodeKind::VariableDeclaration { initializer: Some(init), .. } => {
274                self.visit_node(init);
275            }
276
277            NodeKind::DataSection { marker: _, body } => {
278                // Fold the data section body as a comment
279                if body.is_some() {
280                    self.add_range_from_node(node, Some(FoldingRangeKind::Comment));
281                }
282            }
283
284            NodeKind::LabeledStatement { label: _, statement } => {
285                // Labeled loops (LABEL: while/for/foreach) fold the inner statement
286                self.add_range_from_node(node, None);
287                self.visit_node(statement);
288            }
289
290            NodeKind::Format { .. } => {
291                // Format declarations fold as regions (like heredocs)
292                self.add_range_from_node(node, Some(FoldingRangeKind::Region));
293            }
294
295            NodeKind::Tie { variable, package, args } => {
296                // Tie expressions with arguments are foldable when multi-line
297                self.add_range_from_node(node, None);
298                self.visit_node(variable);
299                self.visit_node(package);
300                for arg in args {
301                    self.visit_node(arg);
302                }
303            }
304
305            // Other node types - visit children if any
306            _ => {}
307        }
308    }
309
310    /// Add a folding range from a node
311    fn add_range_from_node(&mut self, node: &Node, kind: Option<FoldingRangeKind>) {
312        // Use actual offsets from location
313        let start_offset = node.location.start;
314        let end_offset = node.location.end;
315
316        // Only add if it's not trivial
317        if end_offset > start_offset + 1 {
318            self.ranges.push(FoldingRange { start_offset, end_offset, kind });
319        }
320    }
321
322    /// Add a folding range from two locations
323    fn add_range_from_locations(
324        &mut self,
325        start: &SourceLocation,
326        end: &SourceLocation,
327        kind: Option<FoldingRangeKind>,
328    ) {
329        let start_offset = start.start;
330        let end_offset = end.end;
331
332        if end_offset > start_offset + 1 {
333            self.ranges.push(FoldingRange { start_offset, end_offset, kind });
334        }
335    }
336}