Format a docstring by trimming whitespace and adjusting the indentation. Summary of changes we make: Normalize the string like all other strings Ignore docstring that have an escaped newline Trim all trailing whitespace, except for a chaperone space that avoids quotes or backslashes in the last line. Trim leading whitespace on the first line, again except for a chaperone space If there is only co
(normalized: &NormalizedString, f: &mut PyFormatter)
| 111 | /// `indent-width * spaces` to tabs because doing so could break ASCII art and other docstrings |
| 112 | /// that use spaces for alignment. |
| 113 | pub(crate) fn format(normalized: &NormalizedString, f: &mut PyFormatter) -> FormatResult<()> { |
| 114 | let docstring = &normalized.text(); |
| 115 | |
| 116 | // Black doesn't change the indentation of docstrings that contain an escaped newline |
| 117 | if contains_unescaped_newline(docstring) { |
| 118 | return normalized.fmt(f); |
| 119 | } |
| 120 | |
| 121 | // is_borrowed is unstable :/ |
| 122 | let already_normalized = matches!(docstring, Cow::Borrowed(_)); |
| 123 | |
| 124 | // Use `split` instead of `lines` to preserve the closing quotes on their own line |
| 125 | // if they have no indentation (in which case the last line is `\n` which |
| 126 | // `lines` omit for the last element). |
| 127 | let mut lines = docstring.split('\n').peekable(); |
| 128 | |
| 129 | // Start the string |
| 130 | let kind = normalized.flags(); |
| 131 | let quotes = StringQuotes::from(kind); |
| 132 | write!(f, [kind.prefix(), quotes])?; |
| 133 | // We track where in the source docstring we are (in source code byte offsets) |
| 134 | let mut offset = normalized.start(); |
| 135 | |
| 136 | // The first line directly after the opening quotes has different rules than the rest, mainly |
| 137 | // that we remove all leading whitespace as there's no indentation |
| 138 | let first = lines.next().unwrap_or_default(); |
| 139 | // Black trims whitespace using [`str.strip()`](https://docs.python.org/3/library/stdtypes.html#str.strip) |
| 140 | // https://github.com/psf/black/blob/b4dca26c7d93f930bbd5a7b552807370b60d4298/src/black/strings.py#L77-L85 |
| 141 | // So we use the unicode whitespace definition through `trim_{start,end}` instead of the python |
| 142 | // tokenizer whitespace definition in `trim_whitespace_{start,end}`. |
| 143 | let trim_end = first.trim_end(); |
| 144 | let trim_both = trim_end.trim_start(); |
| 145 | |
| 146 | // Edge case: The first line is `""" "content`, so we need to insert chaperone space that keep |
| 147 | // inner quotes and closing quotes from getting to close to avoid `""""content` |
| 148 | if trim_both.starts_with(quotes.quote_char.as_char()) { |
| 149 | space().fmt(f)?; |
| 150 | } |
| 151 | |
| 152 | if !trim_end.is_empty() { |
| 153 | // For the first line of the docstring we strip the leading and trailing whitespace, e.g. |
| 154 | // `""" content ` to `"""content` |
| 155 | let leading_whitespace = trim_end.text_len() - trim_both.text_len(); |
| 156 | let trimmed_line_range = |
| 157 | TextRange::at(offset, trim_end.text_len()).add_start(leading_whitespace); |
| 158 | if already_normalized { |
| 159 | source_text_slice(trimmed_line_range).fmt(f)?; |
| 160 | } else { |
| 161 | text(trim_both).fmt(f)?; |
| 162 | } |
| 163 | } |
| 164 | offset += first.text_len(); |
| 165 | |
| 166 | // Check if we have a single line (or empty) docstring |
| 167 | if docstring[first.len()..].trim().is_empty() { |
| 168 | // For `"""\n"""` or other whitespace between the quotes, black keeps a single whitespace, |
| 169 | // but `""""""` doesn't get one inserted. |
| 170 | if needs_chaperone_space(normalized.flags(), trim_end) |
no test coverage detected