1#![deny(unsafe_code)]
7#![warn(missing_docs)]
8
9use perl_ast::{Node, NodeKind};
10
11#[must_use]
13pub fn find_declaration_position(source: &str, error_pos: usize) -> usize {
14 find_statement_start(source, error_pos)
15}
16
17#[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#[must_use]
37pub fn find_function_insert_position(source: &str) -> usize {
38 source.len()
39}
40
41#[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#[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}