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 safe_pos = (0..=clamped_pos).rev().find(|&i| source.is_char_boundary(i)).unwrap_or(0);
99 let line_start = source[..safe_pos].rfind('\n').map_or(0, |p| p + 1);
100 let line = &source[line_start..];
101
102 let mut indent = String::new();
103 for ch in line.chars() {
104 if ch == ' ' || ch == '\t' {
105 indent.push(ch);
106 } else {
107 break;
108 }
109 }
110 indent
111}
112
113#[cfg(test)]
114mod tests {
115 use super::{find_declaration_position, find_statement_start, get_indent_at};
116
117 #[test]
118 fn finds_statement_start_after_semicolon() {
119 let src = "my $x = 1;\nmy $y = 2;";
120 let pos = src.find("$y").unwrap_or(0);
121 assert_eq!(find_statement_start(src, pos), src.find('\n').unwrap_or(0) + 1);
122 }
123
124 #[test]
125 fn finds_statement_start_when_terminator_is_at_index_zero() {
126 assert_eq!(find_statement_start(";x", 1), 1);
130 assert_eq!(find_statement_start("\nfoo", 1), 1);
131 }
132
133 #[test]
134 fn returns_zero_when_no_terminator_precedes_pos() {
135 assert_eq!(find_statement_start("abc", 3), 0);
136 assert_eq!(find_statement_start("", 0), 0);
137 assert_eq!(find_statement_start("abc", 0), 0);
138 }
139
140 #[test]
141 fn handles_pos_beyond_source_len() {
142 let src = "foo;bar";
143 assert_eq!(find_statement_start(src, 100), 4);
144 }
145
146 #[test]
147 fn declaration_position_delegates_to_statement_start() {
148 let src = "print 'a';\nprint 'b';";
149 let pos = src.find("'b'").unwrap_or(0);
150 assert_eq!(find_declaration_position(src, pos), find_statement_start(src, pos));
151 }
152
153 #[test]
154 fn captures_whitespace_indent() {
155 let src = "if (1) {\n say 'x';\n}\n";
156 let pos = src.find("say").unwrap_or(0);
157 assert_eq!(get_indent_at(src, pos), " ");
158 }
159 #[test]
160 fn get_indent_clamps_out_of_bounds_pos() {
161 let src = "line1\n line2";
162 assert_eq!(get_indent_at(src, src.len() + 50), " ");
163 }
164
165 #[test]
166 fn get_indent_at_mid_emoji_does_not_panic() {
167 let src = " \u{1F600}";
169 let emoji_start = 4;
170 assert_eq!(get_indent_at(src, emoji_start + 1), " ");
172 }
173
174 #[test]
175 fn get_indent_at_mid_cjk_does_not_panic() {
176 let src = "\n \u{4E2D}text";
178 let cjk_start = src.find('\u{4E2D}').unwrap_or(0);
179 assert_eq!(get_indent_at(src, cjk_start), " ");
180 assert_eq!(get_indent_at(src, cjk_start + 1), " ");
182 }
183}