Skip to main content

perl_ast_utils/
lib.rs

1//! AST utilities for Perl LSP microcrates.
2//!
3//! This crate has a narrow responsibility: provide AST and source-text helpers
4//! used by higher-level LSP features (for example, code actions).
5
6#![deny(unsafe_code)]
7#![warn(missing_docs)]
8
9use perl_ast::{Node, NodeKind};
10
11/// Find the best position to insert a declaration.
12#[must_use]
13pub fn find_declaration_position(source: &str, error_pos: usize) -> usize {
14    find_statement_start(source, error_pos)
15}
16
17/// Find the start of the current statement.
18#[must_use]
19pub fn find_statement_start(source: &str, pos: usize) -> usize {
20    let mut i = pos.saturating_sub(1);
21    let bytes = source.as_bytes();
22
23    while i > 0 {
24        if bytes.get(i).is_some_and(|b| *b == b';' || *b == b'\n') {
25            return i + 1;
26        }
27        i = i.saturating_sub(1);
28    }
29
30    0
31}
32
33/// Find a good position to insert a function.
34///
35/// Current policy inserts at end-of-file.
36#[must_use]
37pub fn find_function_insert_position(source: &str) -> usize {
38    source.len()
39}
40
41/// Find the most specific node covering the provided byte range.
42#[allow(clippy::only_used_in_recursion)]
43#[must_use]
44pub fn find_node_at_range(node: &Node, range: (usize, usize)) -> Option<&Node> {
45    if node.location.start <= range.0 && node.location.end >= range.1 {
46        match &node.kind {
47            NodeKind::Program { statements } | NodeKind::Block { statements } => {
48                for stmt in statements {
49                    if let Some(result) = find_node_at_range(stmt, range) {
50                        return Some(result);
51                    }
52                }
53            }
54            NodeKind::If { condition, then_branch, elsif_branches, else_branch } => {
55                if let Some(result) = find_node_at_range(condition, range) {
56                    return Some(result);
57                }
58                if let Some(result) = find_node_at_range(then_branch, range) {
59                    return Some(result);
60                }
61                for (cond, branch) in elsif_branches {
62                    if let Some(result) = find_node_at_range(cond, range) {
63                        return Some(result);
64                    }
65                    if let Some(result) = find_node_at_range(branch, range) {
66                        return Some(result);
67                    }
68                }
69                if let Some(branch) = else_branch
70                    && let Some(result) = find_node_at_range(branch, range)
71                {
72                    return Some(result);
73                }
74            }
75            NodeKind::Binary { left, right, .. } => {
76                if let Some(result) = find_node_at_range(left, range) {
77                    return Some(result);
78                }
79                if let Some(result) = find_node_at_range(right, range) {
80                    return Some(result);
81                }
82            }
83            _ => {}
84        }
85        return Some(node);
86    }
87
88    None
89}
90
91/// Get indentation at a position.
92#[must_use]
93pub fn get_indent_at(source: &str, pos: usize) -> String {
94    let line_start = source[..pos].rfind('\n').map_or(0, |p| p + 1);
95    let line = &source[line_start..];
96
97    let mut indent = String::new();
98    for ch in line.chars() {
99        if ch == ' ' || ch == '\t' {
100            indent.push(ch);
101        } else {
102            break;
103        }
104    }
105    indent
106}
107
108#[cfg(test)]
109mod tests {
110    use super::{find_declaration_position, find_statement_start, get_indent_at};
111
112    #[test]
113    fn finds_statement_start_after_semicolon() {
114        let src = "my $x = 1;\nmy $y = 2;";
115        let pos = src.find("$y").unwrap_or(0);
116        assert_eq!(find_statement_start(src, pos), src.find('\n').unwrap_or(0) + 1);
117    }
118
119    #[test]
120    fn declaration_position_delegates_to_statement_start() {
121        let src = "print 'a';\nprint 'b';";
122        let pos = src.find("'b'").unwrap_or(0);
123        assert_eq!(find_declaration_position(src, pos), find_statement_start(src, pos));
124    }
125
126    #[test]
127    fn captures_whitespace_indent() {
128        let src = "if (1) {\n    say 'x';\n}\n";
129        let pos = src.find("say").unwrap_or(0);
130        assert_eq!(get_indent_at(src, pos), "    ");
131    }
132}