1pub fn ending(text: &str) -> &'static str {
8 if text.contains("\r\n") {
9 "\r\n"
10 } else {
11 "\n"
12 }
13}
14
15pub fn line_start(text: &str, at: usize) -> usize {
17 text[..at].rfind('\n').map_or(0, |i| i + 1)
18}
19
20pub fn next_line_start(text: &str, at: usize) -> usize {
23 text[at..].find('\n').map_or(text.len(), |i| at + i + 1)
24}
25
26pub fn is_blank(line: &str) -> bool {
28 line.chars().all(|c| matches!(c, ' ' | '\t' | '\r' | '\n'))
29}
30
31pub fn is_comment_line(line: &str) -> bool {
33 line.trim_start_matches([' ', '\t']).starts_with("//")
34}
35
36pub fn line_after(text: &str, at: usize, line: &str) -> (usize, String) {
39 let nl = ending(text);
40 let bytes = text.as_bytes();
43 let mut i = at;
44 loop {
45 while i < bytes.len() && matches!(bytes[i], b' ' | b'\t') {
46 i += 1;
47 }
48 if !text[i..].starts_with("/*") {
49 break;
50 }
51 i = text[i + 2..]
52 .find("*/")
53 .map_or(text.len(), |n| i + 2 + n + 2);
54 }
55 let next = next_line_start(text, i);
56 if next == text.len() && !text.ends_with('\n') {
57 (next, format!("{nl}{line}"))
58 } else {
59 (next, format!("{line}{nl}"))
60 }
61}
62
63pub fn line_before_item(text: &str, item_start: usize, line: &str) -> (usize, String) {
67 let nl = ending(text);
68 let mut at = line_start(text, item_start);
69 for &(start, end) in comments_before(text, at).iter().rev() {
73 if end > at {
74 at = at.min(line_start(text, start));
75 continue;
76 }
77 if text[end..at].matches('\n').count() > 1 {
78 break;
79 }
80 at = line_start(text, start);
81 }
82 (at, format!("{line}{nl}{nl}"))
83}
84
85fn comments_before(text: &str, end: usize) -> Vec<(usize, usize)> {
89 let bytes = text.as_bytes();
90 let mut out = Vec::new();
91 let mut i = 0;
92 while i < end {
93 match bytes[i] {
94 b' ' | b'\t' | b'\r' | b'\n' => i += 1,
95 b'/' if bytes.get(i + 1) == Some(&b'/') => {
96 let e = text[i..].find('\n').map_or(text.len(), |n| i + n);
97 out.push((i, e));
98 i = e;
99 }
100 b'/' if bytes.get(i + 1) == Some(&b'*') => {
101 let e = text[i + 2..]
102 .find("*/")
103 .map_or(text.len(), |n| i + 2 + n + 2);
104 out.push((i, e));
105 i = e;
106 }
107 _ => break,
108 }
109 }
110 out
111}
112
113pub fn keyword_start(text: &str, name_start: usize) -> Option<usize> {
117 let bytes = text.as_bytes();
118 let back_over_space = |mut i: usize| {
119 while i > 0 && matches!(bytes[i - 1], b' ' | b'\t') {
120 i -= 1;
121 }
122 i
123 };
124 let back_over_word = |mut i: usize| {
125 while i > 0 && bytes[i - 1].is_ascii_alphabetic() {
126 i -= 1;
127 }
128 i
129 };
130 let end = back_over_space(name_start);
131 let start = back_over_word(end);
132 let keyword = &text[start..end];
133 if !matches!(keyword, "fn" | "record" | "type" | "trait" | "const") {
134 return None;
135 }
136 if keyword == "fn" {
137 let end = back_over_space(start);
138 let word = back_over_word(end);
139 if &text[word..end] == "async" {
140 return Some(word);
141 }
142 }
143 Some(start)
144}
145
146pub fn arm_removal(text: &str, start: usize, end: usize) -> Option<(usize, usize)> {
152 let bytes = text.as_bytes();
153 let skip = |mut i: usize| {
154 while i < bytes.len() && matches!(bytes[i], b' ' | b'\t') {
155 i += 1;
156 }
157 i
158 };
159 let mut after = skip(end);
160 match bytes.get(after) {
161 Some(b',') => after = skip(after + 1),
162 None | Some(b'\r' | b'\n' | b'}') => {}
163 Some(b'/') if bytes.get(after + 1) == Some(&b'/') => {}
164 Some(_) => return None,
165 }
166 let first = line_start(text, start);
167 let alone_before = text[first..start]
168 .bytes()
169 .all(|b| matches!(b, b' ' | b'\t'));
170 let alone_after =
171 text[after..].starts_with("//") || matches!(bytes.get(after), None | Some(b'\r' | b'\n'));
172 if alone_before && alone_after {
173 Some((first, next_line_start(text, after)))
174 } else {
175 Some((start, after))
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182
183 #[test]
184 fn a_line_after_keeps_a_trailing_comment_on_its_line() {
185 let text = "import a // why\nfn main() {}\n";
186 assert_eq!(
187 line_after(text, "import a".len(), "import b::{x}"),
188 (16, "import b::{x}\n".to_string())
189 );
190 let crlf = "import a\r\nfn f() {}\r\n";
191 assert_eq!(
192 line_after(crlf, 8, "import b"),
193 (10, "import b\r\n".to_string())
194 );
195 }
196
197 #[test]
198 fn a_line_after_the_last_line_without_an_ending_gets_one() {
199 assert_eq!(
200 line_after("import a", 8, "import b"),
201 (8, "\nimport b".to_string())
202 );
203 }
204
205 #[test]
206 fn a_line_before_an_item_goes_above_its_comments_and_below_a_header() {
207 let text = "// header\n\n// about f\n/// Doc.\nfn f() {}\n";
208 let item = text.find("/// Doc.").unwrap();
209 assert_eq!(
210 line_before_item(text, item, "import m::{x}"),
211 (11, "import m::{x}\n\n".to_string())
212 );
213 }
214
215 #[test]
216 fn the_keyword_before_a_name_takes_async_with_it() {
217 let text = "async fn go() {}\nrecord P {}\ntype S =\n | A\nfn f() {}\nlet x = 1\n";
218 assert_eq!(keyword_start(text, text.find("go").unwrap()), Some(0));
219 assert_eq!(
220 keyword_start(text, text.find("P {").unwrap()),
221 text.find("record")
222 );
223 assert_eq!(
224 keyword_start(text, text.find("S =").unwrap()),
225 text.find("type")
226 );
227 assert_eq!(
228 keyword_start(text, text.find("f()").unwrap()),
229 text.find("fn f")
230 );
231 assert_eq!(keyword_start(text, text.find("x =").unwrap()), None);
232 }
233
234 #[test]
235 fn an_arm_alone_on_its_lines_goes_with_them() {
236 let text = "match x {\n 1 => a,\n _ => {\n b\n }\n}\n";
237 let start = text.find("1 =>").unwrap();
238 assert_eq!(
239 arm_removal(text, start, start + "1 => a".len()),
240 Some((10, 22))
241 );
242 let start = text.find("_ =>").unwrap();
243 let end = text.rfind(" }").unwrap() + " }".len();
244 assert_eq!(
245 arm_removal(text, start, end).map(|(s, e)| &text[s..e]),
246 Some(" _ => {\n b\n }\n")
247 );
248 let text = "match x {\n 1 => a, // one\n _ => b\n}\n";
250 let start = text.find("1 =>").unwrap();
251 assert_eq!(
252 arm_removal(text, start, start + "1 => a".len()),
253 Some((10, 29))
254 );
255 }
256
257 #[test]
258 fn an_arm_sharing_its_line_goes_alone_with_its_comma() {
259 let text = "match x { 1 => a, 2 => b }";
260 let start = text.find("1 =>").unwrap();
261 assert_eq!(
262 arm_removal(text, start, start + 6).map(|(s, e)| &text[s..e]),
263 Some("1 => a, ")
264 );
265 let start = text.find("2 =>").unwrap();
266 assert_eq!(
267 arm_removal(text, start, start + 6).map(|(s, e)| &text[s..e]),
268 Some("2 => b ")
269 );
270 assert_eq!(arm_removal("1 => a /* c */,", 0, 6), None);
272 }
273
274 #[test]
275 fn the_line_ending_is_the_texts_own() {
276 assert_eq!(ending("a\r\nb\r\n"), "\r\n");
277 assert_eq!(ending("a\nb"), "\n");
278 assert!(is_blank(" \t\r\n") && !is_blank(" x\n"));
279 assert!(is_comment_line(" // c\n") && !is_comment_line("x // c\n"));
280 }
281
282 #[test]
283 fn a_line_after_an_import_goes_past_a_block_comment_that_goes_on() {
284 let text = "import a /* one\n two */\nfn main() {}\n";
286 let after = text.find("two */\n").unwrap() + "two */\n".len();
287 assert_eq!(
288 line_after(text, "import a".len(), "import b::{x}"),
289 (after, "import b::{x}\n".to_string())
290 );
291 let crlf = text.replace('\n', "\r\n");
292 let after = crlf.find("two */\r\n").unwrap() + "two */\r\n".len();
293 assert_eq!(
294 line_after(&crlf, "import a".len(), "import b::{x}"),
295 (after, "import b::{x}\r\n".to_string())
296 );
297 }
298
299 #[test]
300 fn a_line_before_an_item_goes_above_a_block_comment_over_it() {
301 let text = "// header\n\n/* about f\n\n more */\nfn f() {}\n";
304 let item = text.find("fn f").unwrap();
305 assert_eq!(
306 line_before_item(text, item, "import m::{x}"),
307 (
308 text.find("/* about").unwrap(),
309 "import m::{x}\n\n".to_string()
310 )
311 );
312 let crlf = text.replace('\n', "\r\n");
313 let item = crlf.find("fn f").unwrap();
314 assert_eq!(
315 line_before_item(&crlf, item, "import m::{x}"),
316 (
317 crlf.find("/* about").unwrap(),
318 "import m::{x}\r\n\r\n".to_string()
319 )
320 );
321 }
322
323 #[test]
324 fn comments_sharing_a_line_above_an_item_go_with_it() {
325 let two = "/* a */ // b\nfn f() {}\n";
328 assert_eq!(
329 line_before_item(two, two.find("fn f").unwrap(), "import m::{x}"),
330 (0, "import m::{x}\n\n".to_string())
331 );
332 let shared = "/* a\n */ fn f() {}\n";
333 assert_eq!(
334 line_before_item(shared, shared.find("fn f").unwrap(), "import m::{x}"),
335 (0, "import m::{x}\n\n".to_string())
336 );
337 }
338}