Skip to main content

ruff_python_trivia/
textwrap.rs

1//! Functions related to adding and removing indentation from lines of
2//! text.
3
4use std::borrow::Cow;
5use std::cmp;
6
7use ruff_source_file::UniversalNewlines;
8
9use crate::PythonWhitespace;
10
11/// Indent each line by the given prefix.
12///
13/// # Examples
14///
15/// ```
16/// # use ruff_python_trivia::textwrap::indent;
17///
18/// assert_eq!(indent("First line.\nSecond line.\n", "  "),
19///            "  First line.\n  Second line.\n");
20/// ```
21///
22/// When indenting, trailing whitespace is stripped from the prefix.
23/// This means that empty lines remain empty afterwards:
24///
25/// ```
26/// # use ruff_python_trivia::textwrap::indent;
27///
28/// assert_eq!(indent("First line.\n\n\nSecond line.\n", "  "),
29///            "  First line.\n\n\n  Second line.\n");
30/// ```
31///
32/// Notice how `"\n\n\n"` remained as `"\n\n\n"`.
33///
34/// This feature is useful when you want to indent text and have a
35/// space between your prefix and the text. In this case, you _don't_
36/// want a trailing space on empty lines:
37///
38/// ```
39/// # use ruff_python_trivia::textwrap::indent;
40///
41/// assert_eq!(indent("foo = 123\n\nprint(foo)\n", "# "),
42///            "# foo = 123\n#\n# print(foo)\n");
43/// ```
44///
45/// Notice how `"\n\n"` became `"\n#\n"` instead of `"\n# \n"` which
46/// would have trailing whitespace.
47///
48/// Leading and trailing whitespace coming from the text itself is
49/// kept unchanged:
50///
51/// ```
52/// # use ruff_python_trivia::textwrap::indent;
53///
54/// assert_eq!(indent(" \t  Foo   ", "->"), "-> \t  Foo   ");
55/// ```
56pub 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
74/// Indent only the first line by the given prefix.
75///
76/// This function is useful when you want to indent the first line of a multi-line
77/// expression while preserving the relative indentation of subsequent lines.
78///
79/// # Examples
80///
81/// ```
82/// # use ruff_python_trivia::textwrap::indent_first_line;
83///
84/// assert_eq!(indent_first_line("First line.\nSecond line.\n", "  "),
85///            "  First line.\nSecond line.\n");
86/// ```
87///
88/// When indenting, trailing whitespace is stripped from the prefix.
89/// This means that empty lines remain empty afterwards:
90///
91/// ```
92/// # use ruff_python_trivia::textwrap::indent_first_line;
93///
94/// assert_eq!(indent_first_line("\n\n\nSecond line.\n", "  "),
95///            "\n\n\nSecond line.\n");
96/// ```
97///
98/// Leading and trailing whitespace coming from the text itself is
99/// kept unchanged:
100///
101/// ```
102/// # use ruff_python_trivia::textwrap::indent_first_line;
103///
104/// assert_eq!(indent_first_line(" \t  Foo   ", "->"), "-> \t  Foo   ");
105/// ```
106pub 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    // Indent only the first line
119    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    // Add remaining lines without indentation
127    for line in lines {
128        result.push_str(line.as_full_str());
129    }
130
131    Cow::Owned(result)
132}
133
134/// Removes common leading whitespace from each line.
135///
136/// This function will look at each non-empty line and determine the
137/// maximum amount of whitespace that can be removed from all lines.
138///
139/// Lines that consist solely of whitespace are trimmed to a blank line.
140///
141/// ```
142/// # use ruff_python_trivia::textwrap::dedent;
143///
144/// assert_eq!(dedent("
145///     1st line
146///       2nd line
147///     3rd line
148/// "), "
149/// 1st line
150///   2nd line
151/// 3rd line
152/// ");
153/// ```
154pub fn dedent(text: &str) -> Cow<'_, str> {
155    // Find the minimum amount of leading whitespace on each line.
156    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                // Skip empty lines.
162                prefix_len
163            } else {
164                cmp::min(prefix_len, leading_whitespace_len)
165            }
166        });
167
168    // If there is no common prefix, no need to dedent.
169    if prefix_len == usize::MAX {
170        return Cow::Borrowed(text);
171    }
172
173    // Remove the common prefix from each line.
174    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
187/// Reduce a block's indentation to match the provided indentation.
188///
189/// This function looks at the first line in the block to determine the
190/// current indentation, then removes whitespace from each line to
191/// match the provided indentation.
192///
193/// Leading comments are ignored unless the block is only composed of comments.
194///
195/// Lines that are indented by _less_ than the indent of the first line
196/// are left unchanged.
197///
198/// Lines that consist solely of whitespace are trimmed to a blank line.
199///
200/// Lines that start with formfeeds have the indentation after the formfeeds
201/// removed and the formfeeds reinstated
202///
203/// # Panics
204/// If the first line is indented by less than the provided indent.
205pub fn dedent_to(text: &str, indent: &str) -> Option<String> {
206    // The caller may provide an `indent` from source code by taking
207    // a range of text beginning with the start of a line. In Python,
208    // while a line may begin with form feeds, these do not contribute
209    // to the indentation. So we strip those here.
210    let indent = indent.trim_start_matches('\x0C');
211    // Look at the indentation of the first non-empty line, to determine the "baseline" indentation.
212    let mut first_comment_indent = None;
213    let existing_indent_len = text
214        .universal_newlines()
215        .find_map(|line| {
216            // Following Python's lexer, treat form feed character's at the start of a line
217            // the same as a line break (reset the indentation)
218            let trimmed_start_of_line_formfeed = line.trim_start_matches('\x0C');
219            let trimmed = trimmed_start_of_line_formfeed.trim_whitespace_start();
220
221            // A whitespace only line
222            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    // Determine the amount of indentation to remove.
242    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        // Determine the current indentation level.
264        let current_indent_len = line_content.len() - line_without_indent.len();
265
266        if current_indent_len < existing_indent_len {
267            // If the current indentation level is less than the baseline, keep it as is.
268            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}