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> {
206 let indent = indent.trim_start_matches('\x0C');
211 let mut first_comment_indent = None;
213 let existing_indent_len = text
214 .universal_newlines()
215 .find_map(|line| {
216 let trimmed_start_of_line_formfeed = line.trim_start_matches('\x0C');
219 let trimmed = trimmed_start_of_line_formfeed.trim_whitespace_start();
220
221 if trimmed.is_empty() {
223 return None;
224 }
225
226 let indent_len = trimmed_start_of_line_formfeed.len() - trimmed.len();
227
228 if trimmed.starts_with('#') && first_comment_indent.is_none() {
229 first_comment_indent = Some(indent_len);
230 None
231 } else {
232 Some(indent_len)
233 }
234 })
235 .unwrap_or(first_comment_indent.unwrap_or_default());
236
237 if existing_indent_len < indent.len() {
238 return None;
239 }
240
241 let dedent_len = existing_indent_len - indent.len();
243
244 let mut result = String::with_capacity(text.len() + indent.len());
245
246 for line in text.universal_newlines() {
247 let line_content = line.trim_start_matches('\x0C');
248 let formfeed_count = line.len() - line_content.len();
249
250 let line_ending = if let Some(line_ending) = line.line_ending() {
251 line_ending.as_str()
252 } else {
253 ""
254 };
255
256 let line_without_indent = line.trim_whitespace_start();
257
258 if line_without_indent.is_empty() {
259 result.push_str(line_ending);
260 continue;
261 }
262
263 let current_indent_len = line_content.len() - line_without_indent.len();
265
266 if current_indent_len < existing_indent_len {
267 result.push_str(line.as_full_str());
269 continue;
270 }
271 let dedented_content = &line_content[dedent_len..];
272
273 let formfeeds = &line[..formfeed_count];
274 result.push_str(formfeeds);
275 result.push_str(dedented_content);
276 result.push_str(line_ending);
277 }
278 Some(result)
279}
280
281#[cfg(test)]
282mod tests {
283 use super::*;
284
285 #[test]
286 fn indent_empty() {
287 assert_eq!(indent("\n", " "), "\n");
288 }
289
290 #[test]
291 #[rustfmt::skip]
292 fn indent_nonempty() {
293 let text = [
294 " foo\n",
295 "bar\n",
296 " baz\n",
297 ].join("");
298 let expected = [
299 "// foo\n",
300 "// bar\n",
301 "// baz\n",
302 ].join("");
303 assert_eq!(indent(&text, "// "), expected);
304 }
305
306 #[test]
307 #[rustfmt::skip]
308 fn indent_empty_line() {
309 let text = [
310 " foo",
311 "bar",
312 "",
313 " baz",
314 ].join("\n");
315 let expected = [
316 "// foo",
317 "// bar",
318 "//",
319 "// baz",
320 ].join("\n");
321 assert_eq!(indent(&text, "// "), expected);
322 }
323
324 #[test]
325 #[rustfmt::skip]
326 fn indent_mixed_newlines() {
327 let text = [
328 " foo\r\n",
329 "bar\n",
330 " baz\r",
331 ].join("");
332 let expected = [
333 "// foo\r\n",
334 "// bar\n",
335 "// baz\r",
336 ].join("");
337 assert_eq!(indent(&text, "// "), expected);
338 }
339
340 #[test]
341 fn dedent_empty() {
342 assert_eq!(dedent(""), "");
343 }
344
345 #[test]
346 #[rustfmt::skip]
347 fn dedent_multi_line() {
348 let x = [
349 " foo",
350 " bar",
351 " baz",
352 ].join("\n");
353 let y = [
354 " foo",
355 "bar",
356 " baz"
357 ].join("\n");
358 assert_eq!(dedent(&x), y);
359 }
360
361 #[test]
362 #[rustfmt::skip]
363 fn dedent_empty_line() {
364 let x = [
365 " foo",
366 " bar",
367 " ",
368 " baz"
369 ].join("\n");
370 let y = [
371 " foo",
372 "bar",
373 "",
374 " baz"
375 ].join("\n");
376 assert_eq!(dedent(&x), y);
377 }
378
379 #[test]
380 #[rustfmt::skip]
381 fn dedent_blank_line() {
382 let x = [
383 " foo",
384 "",
385 " bar",
386 " foo",
387 " bar",
388 " baz",
389 ].join("\n");
390 let y = [
391 "foo",
392 "",
393 " bar",
394 " foo",
395 " bar",
396 " baz",
397 ].join("\n");
398 assert_eq!(dedent(&x), y);
399 }
400
401 #[test]
402 #[rustfmt::skip]
403 fn dedent_whitespace_line() {
404 let x = [
405 " foo",
406 " ",
407 " bar",
408 " foo",
409 " bar",
410 " baz",
411 ].join("\n");
412 let y = [
413 "foo",
414 "",
415 " bar",
416 " foo",
417 " bar",
418 " baz",
419 ].join("\n");
420 assert_eq!(dedent(&x), y);
421 }
422
423 #[test]
424 #[rustfmt::skip]
425 fn dedent_mixed_whitespace() {
426 let x = [
427 "\tfoo",
428 " bar",
429 ].join("\n");
430 let y = [
431 "foo",
432 " bar",
433 ].join("\n");
434 assert_eq!(dedent(&x), y);
435 }
436
437 #[test]
438 #[rustfmt::skip]
439 fn dedent_tabbed_whitespace() {
440 let x = [
441 "\t\tfoo",
442 "\t\t\tbar",
443 ].join("\n");
444 let y = [
445 "foo",
446 "\tbar",
447 ].join("\n");
448 assert_eq!(dedent(&x), y);
449 }
450
451 #[test]
452 #[rustfmt::skip]
453 fn dedent_mixed_tabbed_whitespace() {
454 let x = [
455 "\t \tfoo",
456 "\t \t\tbar",
457 ].join("\n");
458 let y = [
459 "foo",
460 "\tbar",
461 ].join("\n");
462 assert_eq!(dedent(&x), y);
463 }
464
465 #[test]
466 #[rustfmt::skip]
467 fn dedent_preserve_no_terminating_newline() {
468 let x = [
469 " foo",
470 " bar",
471 ].join("\n");
472 let y = [
473 "foo",
474 " bar",
475 ].join("\n");
476 assert_eq!(dedent(&x), y);
477 }
478
479 #[test]
480 #[rustfmt::skip]
481 fn dedent_mixed_newlines() {
482 let x = [
483 " foo\r\n",
484 " bar\n",
485 " baz\r",
486 ].join("");
487 let y = [
488 " foo\r\n",
489 "bar\n",
490 " baz\r"
491 ].join("");
492 assert_eq!(dedent(&x), y);
493 }
494
495 #[test]
496 fn dedent_non_python_whitespace() {
497 let text = r" C = int(f.rea1,0],[-1,0,1]],
498 [[-1,-1,1],[1,1,-1],[0,-1,0]],
499 [[-1,-1,-1],[1,1,0],[1,0,1]]
500 ]";
501 assert_eq!(dedent(text), text);
502 }
503
504 #[test]
505 fn indent_first_line_empty() {
506 assert_eq!(indent_first_line("\n", " "), "\n");
507 }
508
509 #[test]
510 #[rustfmt::skip]
511 fn indent_first_line_nonempty() {
512 let text = [
513 " foo\n",
514 "bar\n",
515 " baz\n",
516 ].join("");
517 let expected = [
518 "// foo\n",
519 "bar\n",
520 " baz\n",
521 ].join("");
522 assert_eq!(indent_first_line(&text, "// "), expected);
523 }
524
525 #[test]
526 #[rustfmt::skip]
527 fn indent_first_line_empty_line() {
528 let text = [
529 " foo",
530 "bar",
531 "",
532 " baz",
533 ].join("\n");
534 let expected = [
535 "// foo",
536 "bar",
537 "",
538 " baz",
539 ].join("\n");
540 assert_eq!(indent_first_line(&text, "// "), expected);
541 }
542
543 #[test]
544 #[rustfmt::skip]
545 fn indent_first_line_mixed_newlines() {
546 let text = [
547 " foo\r\n",
548 "bar\n",
549 " baz\r",
550 ].join("");
551 let expected = [
552 "// foo\r\n",
553 "bar\n",
554 " baz\r",
555 ].join("");
556 assert_eq!(indent_first_line(&text, "// "), expected);
557 }
558
559 #[test]
560 #[rustfmt::skip]
561 fn adjust_indent() {
562 let x = [
563 " foo",
564 " bar",
565 " ",
566 " baz"
567 ].join("\n");
568 let y = [
569 " foo",
570 " bar",
571 "",
572 " baz"
573 ].join("\n");
574 assert_eq!(dedent_to(&x, " "), Some(y));
575
576 let x = [
577 " foo",
578 " bar",
579 " baz",
580 ].join("\n");
581 let y = [
582 "foo",
583 " bar",
584 "baz"
585 ].join("\n");
586 assert_eq!(dedent_to(&x, ""), Some(y));
587
588 let x = [
589 " # foo",
590 " # bar",
591 "# baz"
592 ].join("\n");
593 let y = [
594 " # foo",
595 " # bar",
596 "# baz"
597 ].join("\n");
598 assert_eq!(dedent_to(&x, " "), Some(y));
599
600 let x = [
601 " # foo",
602 " bar",
603 " baz"
604 ].join("\n");
605 let y = [
606 " # foo",
607 " bar",
608 " baz"
609 ].join("\n");
610 assert_eq!(dedent_to(&x, " "), Some(y));
611
612 let x = [
613 "\x0C 1",
614 " 2"
615 ].join("\n");
616 let y = [
617 "\x0C1",
618 "2"
619 ].join("\n");
620 assert_eq!(dedent_to(&x, ""), Some(y));
621 }
622
623 #[test]
624 #[rustfmt::skip]
625 fn dedent_to_returns_none_if_indent_too_large() {
626 let x = [
627 " foo",
628 " bar"
629 ].join("\n");
630 assert_eq!(dedent_to(&x, " "), None);
631 }
632
633 #[test]
634 #[rustfmt::skip]
635 fn dedent_to_only_whitespace_lines() {
636 let x = [
637 " ",
638 "\t",
639 " "
640 ].join("\n");
641 let y = "\n\n".to_string();
642 assert_eq!(dedent_to(&x, ""), Some(y));
643 }
644
645 #[test]
646 #[rustfmt::skip]
647 fn dedent_to_preserves_crlf_for_lines_starting_with_form_feed() {
648 let x = [
649 "\x0C 1\r\n",
650 " 2\r\n",
651 ].join("");
652 let y = [
653 "\x0C1\r\n",
654 "2\r\n",
655 ].join("");
656 assert_eq!(dedent_to(&x, ""), Some(y));
657 }
658
659 #[test]
660 #[rustfmt::skip]
661 fn dedent_to_preserves_multiple_leading_form_feeds_on_first_line() {
662 let x = [
663 "\x0C\x0C 1",
664 " 2",
665 ].join("\n");
666 let y = [
667 "\x0C\x0C1",
668 "2",
669 ].join("\n");
670 assert_eq!(dedent_to(&x, ""), Some(y));
671 }
672
673 #[test]
674 #[rustfmt::skip]
675 fn dedent_to_preserves_multiple_leading_form_feeds_on_second_line() {
676 let x = [
677 " 1",
678 "\x0C\x0C 2",
679 ].join("\n");
680 let y = [
681 "1",
682 "\x0C\x0C2",
683 ].join("\n");
684 assert_eq!(dedent_to(&x, ""), Some(y));
685 }
686
687 #[test]
688 #[rustfmt::skip]
689 fn dedent_to_handles_when_multiple_leading_form_feeds_greater_than_dedent_len() {
690 let x = [
691 "\x0C\x0C\x0C\x0C 1",
692 " 2",
693 ].join("\n");
694 let y = [
695 "\x0C\x0C\x0C\x0C1",
696 "2",
697 ].join("\n");
698 assert_eq!(dedent_to(&x, ""), Some(y));
699 }
700
701 #[test]
702 #[rustfmt::skip]
703 fn dedent_to_ignores_leading_form_feeds_when_checking_indentation() {
704 let x = [
705 " 1",
706 "\x0C\x0C 2",
707 ].join("\n");
708 let y = [
709 "1",
710 "\x0C\x0C 2",
711 ].join("\n");
712 assert_eq!(dedent_to(&x, ""), Some(y));
713 }
714
715 #[test]
716 #[rustfmt::skip]
717 fn dedent_to_is_idempotent() {
718 let x = [
719 " foo",
720 " bar",
721 " ",
722 " baz"
723 ].join("\n");
724 let y = [
725 " foo",
726 " bar",
727 "",
728 " baz"
729 ].join("\n");
730 let first_result = dedent_to(&x, " ").unwrap();
731 assert_eq!(dedent_to(&first_result, " "), Some(y));
732 }
733
734 #[test]
735 #[rustfmt::skip]
736 fn dedent_to_preserves_less_indented_later_line() {
737 let x = [
738 " foo\n",
739 " bar\n",
740 ].join("");
741 let y = [
742 "foo\n",
743 " bar\n",
744 ].join("");
745 assert_eq!(dedent_to(&x, ""), Some(y));
746 }
747
748 #[test]
749 #[rustfmt::skip]
750 fn dedent_to_preserves_less_indented_later_line_with_crlf() {
751 let x = [
752 " foo\r\n",
753 " bar\r\n",
754 ].join("");
755 let y = [
756 "foo\r\n",
757 " bar\r\n",
758 ].join("");
759 assert_eq!(dedent_to(&x, ""), Some(y));
760 }
761
762 #[test]
763 #[rustfmt::skip]
764 fn dedent_to_ignores_leading_form_feeds_in_provided_indentation() {
765 let x = [
766 " 1",
767 " 2",
768 ].join("\n");
769 let y = [
770 "1",
771 "2",
772 ].join("\n");
773 assert_eq!(dedent_to(&x, "\x0C\x0C"), Some(y));
774 }
775}