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]
22pub fn find_statement_start(source: &str, pos: usize) -> usize {
23 let bytes = source.as_bytes();
24 let start = pos.min(bytes.len());
25
26 for i in (0..start).rev() {
27 if bytes[i] == b';' || bytes[i] == b'\n' {
28 return i + 1;
29 }
30 }
31
32 0
33}
34
35#[must_use]
39pub fn find_function_insert_position(source: &str) -> usize {
40 source.len()
41}
42
43#[allow(clippy::only_used_in_recursion)]
45#[must_use]
46pub fn find_node_at_range(node: &Node, range: (usize, usize)) -> Option<&Node> {
47 if node.location.start <= range.0 && node.location.end >= range.1 {
48 match &node.kind {
49 NodeKind::Program { statements } | NodeKind::Block { statements } => {
50 for stmt in statements {
51 if let Some(result) = find_node_at_range(stmt, range) {
52 return Some(result);
53 }
54 }
55 }
56 NodeKind::If { condition, then_branch, elsif_branches, else_branch, .. } => {
57 if let Some(result) = find_node_at_range(condition, range) {
58 return Some(result);
59 }
60 if let Some(result) = find_node_at_range(then_branch, range) {
61 return Some(result);
62 }
63 for (cond, branch) in elsif_branches {
64 if let Some(result) = find_node_at_range(cond, range) {
65 return Some(result);
66 }
67 if let Some(result) = find_node_at_range(branch, range) {
68 return Some(result);
69 }
70 }
71 if let Some(branch) = else_branch
72 && let Some(result) = find_node_at_range(branch, range)
73 {
74 return Some(result);
75 }
76 }
77 NodeKind::Binary { left, right, .. } => {
78 if let Some(result) = find_node_at_range(left, range) {
79 return Some(result);
80 }
81 if let Some(result) = find_node_at_range(right, range) {
82 return Some(result);
83 }
84 }
85 _ => {}
86 }
87 return Some(node);
88 }
89
90 None
91}
92
93#[must_use]
95pub fn get_indent_at(source: &str, pos: usize) -> String {
96 let clamped_pos = pos.min(source.len());
97 let line_start = source[..clamped_pos].rfind('\n').map_or(0, |p| p + 1);
98 let line = &source[line_start..];
99
100 let mut indent = String::new();
101 for ch in line.chars() {
102 if ch == ' ' || ch == '\t' {
103 indent.push(ch);
104 } else {
105 break;
106 }
107 }
108 indent
109}
110
111#[cfg(test)]
112mod tests {
113 use super::{find_declaration_position, find_statement_start, get_indent_at};
114
115 #[test]
116 fn finds_statement_start_after_semicolon() {
117 let src = "my $x = 1;\nmy $y = 2;";
118 let pos = src.find("$y").unwrap_or(0);
119 assert_eq!(find_statement_start(src, pos), src.find('\n').unwrap_or(0) + 1);
120 }
121
122 #[test]
123 fn finds_statement_start_when_terminator_is_at_index_zero() {
124 assert_eq!(find_statement_start(";x", 1), 1);
128 assert_eq!(find_statement_start("\nfoo", 1), 1);
129 }
130
131 #[test]
132 fn returns_zero_when_no_terminator_precedes_pos() {
133 assert_eq!(find_statement_start("abc", 3), 0);
134 assert_eq!(find_statement_start("", 0), 0);
135 assert_eq!(find_statement_start("abc", 0), 0);
136 }
137
138 #[test]
139 fn handles_pos_beyond_source_len() {
140 let src = "foo;bar";
141 assert_eq!(find_statement_start(src, 100), 4);
142 }
143
144 #[test]
145 fn declaration_position_delegates_to_statement_start() {
146 let src = "print 'a';\nprint 'b';";
147 let pos = src.find("'b'").unwrap_or(0);
148 assert_eq!(find_declaration_position(src, pos), find_statement_start(src, pos));
149 }
150
151 #[test]
152 fn captures_whitespace_indent() {
153 let src = "if (1) {\n say 'x';\n}\n";
154 let pos = src.find("say").unwrap_or(0);
155 assert_eq!(get_indent_at(src, pos), " ");
156 }
157 #[test]
158 fn get_indent_clamps_out_of_bounds_pos() {
159 let src = "line1\n line2";
160 assert_eq!(get_indent_at(src, src.len() + 50), " ");
161 }
162}