1use std::borrow::Cow;
5use std::cmp;
6
7use ruff_source_file::UniversalNewlines;
8
9use crate::PythonWhitespace;
10
11pub fn indent<'a>(text: &'a str, prefix: &str) -> Cow<'a, str> {
57 if prefix.is_empty() {
58 return Cow::Borrowed(text);
59 }
60
61 let mut result = String::with_capacity(text.len() + prefix.len());
62 let trimmed_prefix = prefix.trim_whitespace_end();
63 for line in text.universal_newlines() {
64 if line.trim_whitespace().is_empty() {
65 result.push_str(trimmed_prefix);
66 } else {
67 result.push_str(prefix);
68 }
69 result.push_str(line.as_full_str());
70 }
71 Cow::Owned(result)
72}
73
74pub fn indent_first_line<'a>(text: &'a str, prefix: &str) -> Cow<'a, str> {
107 if prefix.is_empty() {
108 return Cow::Borrowed(text);
109 }
110
111 let mut lines = text.universal_newlines();
112 let Some(first_line) = lines.next() else {
113 return Cow::Borrowed(text);
114 };
115
116 let mut result = String::with_capacity(text.len() + prefix.len());
117
118 if first_line.trim_whitespace().is_empty() {
120 result.push_str(prefix.trim_whitespace_end());
121 } else {
122 result.push_str(prefix);
123 }
124 result.push_str(first_line.as_full_str());
125
126 for line in lines {
128 result.push_str(line.as_full_str());
129 }
130
131 Cow::Owned(result)
132}
133
134pub fn dedent(text: &str) -> Cow<'_, str> {
155 let prefix_len = text
157 .universal_newlines()
158 .fold(usize::MAX, |prefix_len, line| {
159 let leading_whitespace_len = line.len() - line.trim_whitespace_start().len();
160 if leading_whitespace_len == line.len() {
161 prefix_len
163 } else {
164 cmp::min(prefix_len, leading_whitespace_len)
165 }
166 });
167
168 if prefix_len == usize::MAX {
170 return Cow::Borrowed(text);
171 }
172
173 let mut result = String::with_capacity(text.len());
175 for line in text.universal_newlines() {
176 if line.trim_whitespace().is_empty() {
177 if let Some(line_ending) = line.line_ending() {
178 result.push_str(&line_ending);
179 }
180 } else {
181 result.push_str(&line.as_full_str()[prefix_len..]);
182 }
183 }
184 Cow::Owned(result)
185}
186
187pub fn dedent_to(text: &str, indent: &str) -> Option<String> {
203 let mut first_comment = None;
205 let existing_indent_len = text
206 .universal_newlines()
207 .find_map(|line| {
208 let trimmed = line.trim_whitespace_start();
209 if trimmed.is_empty() {
210 None
211 } else if trimmed.starts_with('#') && first_comment.is_none() {
212 first_comment = Some(line.len() - trimmed.len());
213 None
214 } else {
215 Some(line.len() - trimmed.len())
216 }
217 })
218 .unwrap_or(first_comment.unwrap_or_default());
219
220 if existing_indent_len < indent.len() {
221 return None;
222 }
223
224 let dedent_len = existing_indent_len - indent.len();
226
227 let mut result = String::with_capacity(text.len() + indent.len());
228 for line in text.universal_newlines() {
229 let trimmed = line.trim_whitespace_start();
230 if trimmed.is_empty() {
231 if let Some(line_ending) = line.line_ending() {
232 result.push_str(&line_ending);
233 }
234 } else {
235 let current_indent_len = line.len() - trimmed.len();
237 if current_indent_len < existing_indent_len {
238 result.push_str(line.as_full_str());
240 } else {
241 result.push_str(&line.as_full_str()[dedent_len..]);
243 }
244 }
245 }
246 Some(result)
247}
248
249#[cfg(test)]
250mod tests {
251 use super::*;
252
253 #[test]
254 fn indent_empty() {
255 assert_eq!(indent("\n", " "), "\n");
256 }
257
258 #[test]
259 #[rustfmt::skip]
260 fn indent_nonempty() {
261 let text = [
262 " foo\n",
263 "bar\n",
264 " baz\n",
265 ].join("");
266 let expected = [
267 "// foo\n",
268 "// bar\n",
269 "// baz\n",
270 ].join("");
271 assert_eq!(indent(&text, "// "), expected);
272 }
273
274 #[test]
275 #[rustfmt::skip]
276 fn indent_empty_line() {
277 let text = [
278 " foo",
279 "bar",
280 "",
281 " baz",
282 ].join("\n");
283 let expected = [
284 "// foo",
285 "// bar",
286 "//",
287 "// baz",
288 ].join("\n");
289 assert_eq!(indent(&text, "// "), expected);
290 }
291
292 #[test]
293 #[rustfmt::skip]
294 fn indent_mixed_newlines() {
295 let text = [
296 " foo\r\n",
297 "bar\n",
298 " baz\r",
299 ].join("");
300 let expected = [
301 "// foo\r\n",
302 "// bar\n",
303 "// baz\r",
304 ].join("");
305 assert_eq!(indent(&text, "// "), expected);
306 }
307
308 #[test]
309 fn dedent_empty() {
310 assert_eq!(dedent(""), "");
311 }
312
313 #[test]
314 #[rustfmt::skip]
315 fn dedent_multi_line() {
316 let x = [
317 " foo",
318 " bar",
319 " baz",
320 ].join("\n");
321 let y = [
322 " foo",
323 "bar",
324 " baz"
325 ].join("\n");
326 assert_eq!(dedent(&x), y);
327 }
328
329 #[test]
330 #[rustfmt::skip]
331 fn dedent_empty_line() {
332 let x = [
333 " foo",
334 " bar",
335 " ",
336 " baz"
337 ].join("\n");
338 let y = [
339 " foo",
340 "bar",
341 "",
342 " baz"
343 ].join("\n");
344 assert_eq!(dedent(&x), y);
345 }
346
347 #[test]
348 #[rustfmt::skip]
349 fn dedent_blank_line() {
350 let x = [
351 " foo",
352 "",
353 " bar",
354 " foo",
355 " bar",
356 " baz",
357 ].join("\n");
358 let y = [
359 "foo",
360 "",
361 " bar",
362 " foo",
363 " bar",
364 " baz",
365 ].join("\n");
366 assert_eq!(dedent(&x), y);
367 }
368
369 #[test]
370 #[rustfmt::skip]
371 fn dedent_whitespace_line() {
372 let x = [
373 " foo",
374 " ",
375 " bar",
376 " foo",
377 " bar",
378 " baz",
379 ].join("\n");
380 let y = [
381 "foo",
382 "",
383 " bar",
384 " foo",
385 " bar",
386 " baz",
387 ].join("\n");
388 assert_eq!(dedent(&x), y);
389 }
390
391 #[test]
392 #[rustfmt::skip]
393 fn dedent_mixed_whitespace() {
394 let x = [
395 "\tfoo",
396 " bar",
397 ].join("\n");
398 let y = [
399 "foo",
400 " bar",
401 ].join("\n");
402 assert_eq!(dedent(&x), y);
403 }
404
405 #[test]
406 #[rustfmt::skip]
407 fn dedent_tabbed_whitespace() {
408 let x = [
409 "\t\tfoo",
410 "\t\t\tbar",
411 ].join("\n");
412 let y = [
413 "foo",
414 "\tbar",
415 ].join("\n");
416 assert_eq!(dedent(&x), y);
417 }
418
419 #[test]
420 #[rustfmt::skip]
421 fn dedent_mixed_tabbed_whitespace() {
422 let x = [
423 "\t \tfoo",
424 "\t \t\tbar",
425 ].join("\n");
426 let y = [
427 "foo",
428 "\tbar",
429 ].join("\n");
430 assert_eq!(dedent(&x), y);
431 }
432
433 #[test]
434 #[rustfmt::skip]
435 fn dedent_preserve_no_terminating_newline() {
436 let x = [
437 " foo",
438 " bar",
439 ].join("\n");
440 let y = [
441 "foo",
442 " bar",
443 ].join("\n");
444 assert_eq!(dedent(&x), y);
445 }
446
447 #[test]
448 #[rustfmt::skip]
449 fn dedent_mixed_newlines() {
450 let x = [
451 " foo\r\n",
452 " bar\n",
453 " baz\r",
454 ].join("");
455 let y = [
456 " foo\r\n",
457 "bar\n",
458 " baz\r"
459 ].join("");
460 assert_eq!(dedent(&x), y);
461 }
462
463 #[test]
464 fn dedent_non_python_whitespace() {
465 let text = r" C = int(f.rea1,0],[-1,0,1]],
466 [[-1,-1,1],[1,1,-1],[0,-1,0]],
467 [[-1,-1,-1],[1,1,0],[1,0,1]]
468 ]";
469 assert_eq!(dedent(text), text);
470 }
471
472 #[test]
473 fn indent_first_line_empty() {
474 assert_eq!(indent_first_line("\n", " "), "\n");
475 }
476
477 #[test]
478 #[rustfmt::skip]
479 fn indent_first_line_nonempty() {
480 let text = [
481 " foo\n",
482 "bar\n",
483 " baz\n",
484 ].join("");
485 let expected = [
486 "// foo\n",
487 "bar\n",
488 " baz\n",
489 ].join("");
490 assert_eq!(indent_first_line(&text, "// "), expected);
491 }
492
493 #[test]
494 #[rustfmt::skip]
495 fn indent_first_line_empty_line() {
496 let text = [
497 " foo",
498 "bar",
499 "",
500 " baz",
501 ].join("\n");
502 let expected = [
503 "// foo",
504 "bar",
505 "",
506 " baz",
507 ].join("\n");
508 assert_eq!(indent_first_line(&text, "// "), expected);
509 }
510
511 #[test]
512 #[rustfmt::skip]
513 fn indent_first_line_mixed_newlines() {
514 let text = [
515 " foo\r\n",
516 "bar\n",
517 " baz\r",
518 ].join("");
519 let expected = [
520 "// foo\r\n",
521 "bar\n",
522 " baz\r",
523 ].join("");
524 assert_eq!(indent_first_line(&text, "// "), expected);
525 }
526
527 #[test]
528 #[rustfmt::skip]
529 fn adjust_indent() {
530 let x = [
531 " foo",
532 " bar",
533 " ",
534 " baz"
535 ].join("\n");
536 let y = [
537 " foo",
538 " bar",
539 "",
540 " baz"
541 ].join("\n");
542 assert_eq!(dedent_to(&x, " "), Some(y));
543
544 let x = [
545 " foo",
546 " bar",
547 " baz",
548 ].join("\n");
549 let y = [
550 "foo",
551 " bar",
552 "baz"
553 ].join("\n");
554 assert_eq!(dedent_to(&x, ""), Some(y));
555
556 let x = [
557 " # foo",
558 " # bar",
559 "# baz"
560 ].join("\n");
561 let y = [
562 " # foo",
563 " # bar",
564 "# baz"
565 ].join("\n");
566 assert_eq!(dedent_to(&x, " "), Some(y));
567
568 let x = [
569 " # foo",
570 " bar",
571 " baz"
572 ].join("\n");
573 let y = [
574 " # foo",
575 " bar",
576 " baz"
577 ].join("\n");
578 assert_eq!(dedent_to(&x, " "), Some(y));
579 }
580}