1 //! Format list-like expressions and items.
2
3 use std::cmp;
4 use std::iter::Peekable;
5
6 use rustc_span::BytePos;
7
8 use crate::comment::{find_comment_end, rewrite_comment, FindUncommented};
9 use crate::config::lists::*;
10 use crate::config::{Config, IndentStyle};
11 use crate::rewrite::RewriteContext;
12 use crate::shape::{Indent, Shape};
13 use crate::utils::{
14 count_newlines, first_line_width, last_line_width, mk_sp, starts_with_newline,
15 unicode_str_width,
16 };
17 use crate::visitor::SnippetProvider;
18
19 pub(crate) struct ListFormatting<'a> {
20 tactic: DefinitiveListTactic,
21 separator: &'a str,
22 trailing_separator: SeparatorTactic,
23 separator_place: SeparatorPlace,
24 shape: Shape,
25 // Non-expressions, e.g., items, will have a new line at the end of the list.
26 // Important for comment styles.
27 ends_with_newline: bool,
28 // Remove newlines between list elements for expressions.
29 preserve_newline: bool,
30 // Nested import lists get some special handling for the "Mixed" list type
31 nested: bool,
32 // Whether comments should be visually aligned.
33 align_comments: bool,
34 config: &'a Config,
35 }
36
37 impl<'a> ListFormatting<'a> {
new(shape: Shape, config: &'a Config) -> Self38 pub(crate) fn new(shape: Shape, config: &'a Config) -> Self {
39 ListFormatting {
40 tactic: DefinitiveListTactic::Vertical,
41 separator: ",",
42 trailing_separator: SeparatorTactic::Never,
43 separator_place: SeparatorPlace::Back,
44 shape,
45 ends_with_newline: true,
46 preserve_newline: false,
47 nested: false,
48 align_comments: true,
49 config,
50 }
51 }
52
tactic(mut self, tactic: DefinitiveListTactic) -> Self53 pub(crate) fn tactic(mut self, tactic: DefinitiveListTactic) -> Self {
54 self.tactic = tactic;
55 self
56 }
57
separator(mut self, separator: &'a str) -> Self58 pub(crate) fn separator(mut self, separator: &'a str) -> Self {
59 self.separator = separator;
60 self
61 }
62
trailing_separator(mut self, trailing_separator: SeparatorTactic) -> Self63 pub(crate) fn trailing_separator(mut self, trailing_separator: SeparatorTactic) -> Self {
64 self.trailing_separator = trailing_separator;
65 self
66 }
67
separator_place(mut self, separator_place: SeparatorPlace) -> Self68 pub(crate) fn separator_place(mut self, separator_place: SeparatorPlace) -> Self {
69 self.separator_place = separator_place;
70 self
71 }
72
ends_with_newline(mut self, ends_with_newline: bool) -> Self73 pub(crate) fn ends_with_newline(mut self, ends_with_newline: bool) -> Self {
74 self.ends_with_newline = ends_with_newline;
75 self
76 }
77
preserve_newline(mut self, preserve_newline: bool) -> Self78 pub(crate) fn preserve_newline(mut self, preserve_newline: bool) -> Self {
79 self.preserve_newline = preserve_newline;
80 self
81 }
82
nested(mut self, nested: bool) -> Self83 pub(crate) fn nested(mut self, nested: bool) -> Self {
84 self.nested = nested;
85 self
86 }
87
align_comments(mut self, align_comments: bool) -> Self88 pub(crate) fn align_comments(mut self, align_comments: bool) -> Self {
89 self.align_comments = align_comments;
90 self
91 }
92
needs_trailing_separator(&self) -> bool93 pub(crate) fn needs_trailing_separator(&self) -> bool {
94 match self.trailing_separator {
95 // We always put separator in front.
96 SeparatorTactic::Always => true,
97 SeparatorTactic::Vertical => self.tactic == DefinitiveListTactic::Vertical,
98 SeparatorTactic::Never => {
99 self.tactic == DefinitiveListTactic::Vertical && self.separator_place.is_front()
100 }
101 }
102 }
103 }
104
105 impl AsRef<ListItem> for ListItem {
as_ref(&self) -> &ListItem106 fn as_ref(&self) -> &ListItem {
107 self
108 }
109 }
110
111 #[derive(PartialEq, Eq, Debug, Copy, Clone)]
112 pub(crate) enum ListItemCommentStyle {
113 // Try to keep the comment on the same line with the item.
114 SameLine,
115 // Put the comment on the previous or the next line of the item.
116 DifferentLine,
117 // No comment available.
118 None,
119 }
120
121 #[derive(Debug, Clone)]
122 pub(crate) struct ListItem {
123 // None for comments mean that they are not present.
124 pub(crate) pre_comment: Option<String>,
125 pub(crate) pre_comment_style: ListItemCommentStyle,
126 // Item should include attributes and doc comments. None indicates a failed
127 // rewrite.
128 pub(crate) item: Option<String>,
129 pub(crate) post_comment: Option<String>,
130 // Whether there is extra whitespace before this item.
131 pub(crate) new_lines: bool,
132 }
133
134 impl ListItem {
empty() -> ListItem135 pub(crate) fn empty() -> ListItem {
136 ListItem {
137 pre_comment: None,
138 pre_comment_style: ListItemCommentStyle::None,
139 item: None,
140 post_comment: None,
141 new_lines: false,
142 }
143 }
144
inner_as_ref(&self) -> &str145 pub(crate) fn inner_as_ref(&self) -> &str {
146 self.item.as_ref().map_or("", |s| s)
147 }
148
is_different_group(&self) -> bool149 pub(crate) fn is_different_group(&self) -> bool {
150 self.inner_as_ref().contains('\n')
151 || self.pre_comment.is_some()
152 || self
153 .post_comment
154 .as_ref()
155 .map_or(false, |s| s.contains('\n'))
156 }
157
is_multiline(&self) -> bool158 pub(crate) fn is_multiline(&self) -> bool {
159 self.inner_as_ref().contains('\n')
160 || self
161 .pre_comment
162 .as_ref()
163 .map_or(false, |s| s.contains('\n'))
164 || self
165 .post_comment
166 .as_ref()
167 .map_or(false, |s| s.contains('\n'))
168 }
169
has_single_line_comment(&self) -> bool170 pub(crate) fn has_single_line_comment(&self) -> bool {
171 self.pre_comment
172 .as_ref()
173 .map_or(false, |comment| comment.trim_start().starts_with("//"))
174 || self
175 .post_comment
176 .as_ref()
177 .map_or(false, |comment| comment.trim_start().starts_with("//"))
178 }
179
has_comment(&self) -> bool180 pub(crate) fn has_comment(&self) -> bool {
181 self.pre_comment.is_some() || self.post_comment.is_some()
182 }
183
from_str<S: Into<String>>(s: S) -> ListItem184 pub(crate) fn from_str<S: Into<String>>(s: S) -> ListItem {
185 ListItem {
186 pre_comment: None,
187 pre_comment_style: ListItemCommentStyle::None,
188 item: Some(s.into()),
189 post_comment: None,
190 new_lines: false,
191 }
192 }
193
194 // Returns `true` if the item causes something to be written.
is_substantial(&self) -> bool195 fn is_substantial(&self) -> bool {
196 fn empty(s: &Option<String>) -> bool {
197 !matches!(*s, Some(ref s) if !s.is_empty())
198 }
199
200 !(empty(&self.pre_comment) && empty(&self.item) && empty(&self.post_comment))
201 }
202 }
203
204 /// The type of separator for lists.
205 #[derive(Copy, Clone, Eq, PartialEq, Debug)]
206 pub(crate) enum Separator {
207 Comma,
208 VerticalBar,
209 }
210
211 impl Separator {
len(self) -> usize212 pub(crate) fn len(self) -> usize {
213 match self {
214 // 2 = `, `
215 Separator::Comma => 2,
216 // 3 = ` | `
217 Separator::VerticalBar => 3,
218 }
219 }
220 }
221
definitive_tactic<I, T>( items: I, tactic: ListTactic, sep: Separator, width: usize, ) -> DefinitiveListTactic where I: IntoIterator<Item = T> + Clone, T: AsRef<ListItem>,222 pub(crate) fn definitive_tactic<I, T>(
223 items: I,
224 tactic: ListTactic,
225 sep: Separator,
226 width: usize,
227 ) -> DefinitiveListTactic
228 where
229 I: IntoIterator<Item = T> + Clone,
230 T: AsRef<ListItem>,
231 {
232 let pre_line_comments = items
233 .clone()
234 .into_iter()
235 .any(|item| item.as_ref().has_single_line_comment());
236
237 let limit = match tactic {
238 _ if pre_line_comments => return DefinitiveListTactic::Vertical,
239 ListTactic::Horizontal => return DefinitiveListTactic::Horizontal,
240 ListTactic::Vertical => return DefinitiveListTactic::Vertical,
241 ListTactic::LimitedHorizontalVertical(limit) => ::std::cmp::min(width, limit),
242 ListTactic::Mixed | ListTactic::HorizontalVertical => width,
243 };
244
245 let (sep_count, total_width) = calculate_width(items.clone());
246 let total_sep_len = sep.len() * sep_count.saturating_sub(1);
247 let real_total = total_width + total_sep_len;
248
249 if real_total <= limit && !items.into_iter().any(|item| item.as_ref().is_multiline()) {
250 DefinitiveListTactic::Horizontal
251 } else {
252 match tactic {
253 ListTactic::Mixed => DefinitiveListTactic::Mixed,
254 _ => DefinitiveListTactic::Vertical,
255 }
256 }
257 }
258
259 // Format a list of commented items into a string.
write_list<I, T>(items: I, formatting: &ListFormatting<'_>) -> Option<String> where I: IntoIterator<Item = T> + Clone, T: AsRef<ListItem>,260 pub(crate) fn write_list<I, T>(items: I, formatting: &ListFormatting<'_>) -> Option<String>
261 where
262 I: IntoIterator<Item = T> + Clone,
263 T: AsRef<ListItem>,
264 {
265 let tactic = formatting.tactic;
266 let sep_len = formatting.separator.len();
267
268 // Now that we know how we will layout, we can decide for sure if there
269 // will be a trailing separator.
270 let mut trailing_separator = formatting.needs_trailing_separator();
271 let mut result = String::with_capacity(128);
272 let cloned_items = items.clone();
273 let mut iter = items.into_iter().enumerate().peekable();
274 let mut item_max_width: Option<usize> = None;
275 let sep_place =
276 SeparatorPlace::from_tactic(formatting.separator_place, tactic, formatting.separator);
277 let mut prev_item_had_post_comment = false;
278 let mut prev_item_is_nested_import = false;
279
280 let mut line_len = 0;
281 let indent_str = &formatting.shape.indent.to_string(formatting.config);
282 while let Some((i, item)) = iter.next() {
283 let item = item.as_ref();
284 let inner_item = item.item.as_ref()?;
285 let first = i == 0;
286 let last = iter.peek().is_none();
287 let mut separate = match sep_place {
288 SeparatorPlace::Front => !first,
289 SeparatorPlace::Back => !last || trailing_separator,
290 };
291 let item_sep_len = if separate { sep_len } else { 0 };
292
293 // Item string may be multi-line. Its length (used for block comment alignment)
294 // should be only the length of the last line.
295 let item_last_line = if item.is_multiline() {
296 inner_item.lines().last().unwrap_or("")
297 } else {
298 inner_item.as_ref()
299 };
300 let mut item_last_line_width = item_last_line.len() + item_sep_len;
301 if item_last_line.starts_with(&**indent_str) {
302 item_last_line_width -= indent_str.len();
303 }
304
305 if !item.is_substantial() {
306 continue;
307 }
308
309 match tactic {
310 DefinitiveListTactic::Horizontal if !first => {
311 result.push(' ');
312 }
313 DefinitiveListTactic::SpecialMacro(num_args_before) => {
314 if i == 0 {
315 // Nothing
316 } else if i < num_args_before {
317 result.push(' ');
318 } else if i <= num_args_before + 1 {
319 result.push('\n');
320 result.push_str(indent_str);
321 } else {
322 result.push(' ');
323 }
324 }
325 DefinitiveListTactic::Vertical
326 if !first && !inner_item.is_empty() && !result.is_empty() =>
327 {
328 result.push('\n');
329 result.push_str(indent_str);
330 }
331 DefinitiveListTactic::Mixed => {
332 let total_width = total_item_width(item) + item_sep_len;
333
334 // 1 is space between separator and item.
335 if (line_len > 0 && line_len + 1 + total_width > formatting.shape.width)
336 || prev_item_had_post_comment
337 || (formatting.nested
338 && (prev_item_is_nested_import || (!first && inner_item.contains("::"))))
339 {
340 result.push('\n');
341 result.push_str(indent_str);
342 line_len = 0;
343 if formatting.ends_with_newline {
344 trailing_separator = true;
345 }
346 } else if line_len > 0 {
347 result.push(' ');
348 line_len += 1;
349 }
350
351 if last && formatting.ends_with_newline {
352 separate = formatting.trailing_separator != SeparatorTactic::Never;
353 }
354
355 line_len += total_width;
356 }
357 _ => {}
358 }
359
360 // Pre-comments
361 if let Some(ref comment) = item.pre_comment {
362 // Block style in non-vertical mode.
363 let block_mode = tactic == DefinitiveListTactic::Horizontal;
364 // Width restriction is only relevant in vertical mode.
365 let comment =
366 rewrite_comment(comment, block_mode, formatting.shape, formatting.config)?;
367 result.push_str(&comment);
368
369 if !inner_item.is_empty() {
370 use DefinitiveListTactic::*;
371 if matches!(tactic, Vertical | Mixed | SpecialMacro(_)) {
372 // We cannot keep pre-comments on the same line if the comment is normalized.
373 let keep_comment = if formatting.config.normalize_comments()
374 || item.pre_comment_style == ListItemCommentStyle::DifferentLine
375 {
376 false
377 } else {
378 // We will try to keep the comment on the same line with the item here.
379 // 1 = ` `
380 let total_width = total_item_width(item) + item_sep_len + 1;
381 total_width <= formatting.shape.width
382 };
383 if keep_comment {
384 result.push(' ');
385 } else {
386 result.push('\n');
387 result.push_str(indent_str);
388 // This is the width of the item (without comments).
389 line_len = item.item.as_ref().map_or(0, |s| unicode_str_width(s));
390 }
391 } else {
392 result.push(' ')
393 }
394 }
395 item_max_width = None;
396 }
397
398 if separate && sep_place.is_front() && !first {
399 result.push_str(formatting.separator.trim());
400 result.push(' ');
401 }
402 result.push_str(inner_item);
403
404 // Post-comments
405 if tactic == DefinitiveListTactic::Horizontal && item.post_comment.is_some() {
406 let comment = item.post_comment.as_ref().unwrap();
407 let formatted_comment = rewrite_comment(
408 comment,
409 true,
410 Shape::legacy(formatting.shape.width, Indent::empty()),
411 formatting.config,
412 )?;
413
414 result.push(' ');
415 result.push_str(&formatted_comment);
416 }
417
418 if separate && sep_place.is_back() {
419 result.push_str(formatting.separator);
420 }
421
422 if tactic != DefinitiveListTactic::Horizontal && item.post_comment.is_some() {
423 let comment = item.post_comment.as_ref().unwrap();
424 let overhead = last_line_width(&result) + first_line_width(comment.trim());
425
426 let rewrite_post_comment = |item_max_width: &mut Option<usize>| {
427 if item_max_width.is_none() && !last && !inner_item.contains('\n') {
428 *item_max_width = Some(max_width_of_item_with_post_comment(
429 &cloned_items,
430 i,
431 overhead,
432 formatting.config.max_width(),
433 ));
434 }
435 let overhead = if starts_with_newline(comment) {
436 0
437 } else if let Some(max_width) = *item_max_width {
438 max_width + 2
439 } else {
440 // 1 = space between item and comment.
441 item_last_line_width + 1
442 };
443 let width = formatting.shape.width.checked_sub(overhead).unwrap_or(1);
444 let offset = formatting.shape.indent + overhead;
445 let comment_shape = Shape::legacy(width, offset);
446
447 // Use block-style only for the last item or multiline comments.
448 let block_style = !formatting.ends_with_newline && last
449 || comment.trim().contains('\n')
450 || comment.trim().len() > width;
451
452 rewrite_comment(
453 comment.trim_start(),
454 block_style,
455 comment_shape,
456 formatting.config,
457 )
458 };
459
460 let mut formatted_comment = rewrite_post_comment(&mut item_max_width)?;
461
462 if !starts_with_newline(comment) {
463 if formatting.align_comments {
464 let mut comment_alignment =
465 post_comment_alignment(item_max_width, inner_item.len());
466 if first_line_width(&formatted_comment)
467 + last_line_width(&result)
468 + comment_alignment
469 + 1
470 > formatting.config.max_width()
471 {
472 item_max_width = None;
473 formatted_comment = rewrite_post_comment(&mut item_max_width)?;
474 comment_alignment =
475 post_comment_alignment(item_max_width, inner_item.len());
476 }
477 for _ in 0..=comment_alignment {
478 result.push(' ');
479 }
480 }
481 // An additional space for the missing trailing separator (or
482 // if we skipped alignment above).
483 if !formatting.align_comments
484 || (last
485 && item_max_width.is_some()
486 && !separate
487 && !formatting.separator.is_empty())
488 {
489 result.push(' ');
490 }
491 } else {
492 result.push('\n');
493 result.push_str(indent_str);
494 }
495 if formatted_comment.contains('\n') {
496 item_max_width = None;
497 }
498 result.push_str(&formatted_comment);
499 } else {
500 item_max_width = None;
501 }
502
503 if formatting.preserve_newline
504 && !last
505 && tactic == DefinitiveListTactic::Vertical
506 && item.new_lines
507 {
508 item_max_width = None;
509 result.push('\n');
510 }
511
512 prev_item_had_post_comment = item.post_comment.is_some();
513 prev_item_is_nested_import = inner_item.contains("::");
514 }
515
516 Some(result)
517 }
518
max_width_of_item_with_post_comment<I, T>( items: &I, i: usize, overhead: usize, max_budget: usize, ) -> usize where I: IntoIterator<Item = T> + Clone, T: AsRef<ListItem>,519 fn max_width_of_item_with_post_comment<I, T>(
520 items: &I,
521 i: usize,
522 overhead: usize,
523 max_budget: usize,
524 ) -> usize
525 where
526 I: IntoIterator<Item = T> + Clone,
527 T: AsRef<ListItem>,
528 {
529 let mut max_width = 0;
530 let mut first = true;
531 for item in items.clone().into_iter().skip(i) {
532 let item = item.as_ref();
533 let inner_item_width = item.inner_as_ref().len();
534 if !first
535 && (item.is_different_group()
536 || item.post_comment.is_none()
537 || inner_item_width + overhead > max_budget)
538 {
539 return max_width;
540 }
541 if max_width < inner_item_width {
542 max_width = inner_item_width;
543 }
544 if item.new_lines {
545 return max_width;
546 }
547 first = false;
548 }
549 max_width
550 }
551
post_comment_alignment(item_max_width: Option<usize>, inner_item_len: usize) -> usize552 fn post_comment_alignment(item_max_width: Option<usize>, inner_item_len: usize) -> usize {
553 item_max_width.unwrap_or(0).saturating_sub(inner_item_len)
554 }
555
556 pub(crate) struct ListItems<'a, I, F1, F2, F3>
557 where
558 I: Iterator,
559 {
560 snippet_provider: &'a SnippetProvider,
561 inner: Peekable<I>,
562 get_lo: F1,
563 get_hi: F2,
564 get_item_string: F3,
565 prev_span_end: BytePos,
566 next_span_start: BytePos,
567 terminator: &'a str,
568 separator: &'a str,
569 leave_last: bool,
570 }
571
extract_pre_comment(pre_snippet: &str) -> (Option<String>, ListItemCommentStyle)572 pub(crate) fn extract_pre_comment(pre_snippet: &str) -> (Option<String>, ListItemCommentStyle) {
573 let trimmed_pre_snippet = pre_snippet.trim();
574 // Both start and end are checked to support keeping a block comment inline with
575 // the item, even if there are preceeding line comments, while still supporting
576 // a snippet that starts with a block comment but also contains one or more
577 // trailing single line comments.
578 // https://github.com/rust-lang/rustfmt/issues/3025
579 // https://github.com/rust-lang/rustfmt/pull/3048
580 // https://github.com/rust-lang/rustfmt/issues/3839
581 let starts_with_block_comment = trimmed_pre_snippet.starts_with("/*");
582 let ends_with_block_comment = trimmed_pre_snippet.ends_with("*/");
583 let starts_with_single_line_comment = trimmed_pre_snippet.starts_with("//");
584 if ends_with_block_comment {
585 let comment_end = pre_snippet.rfind(|c| c == '/').unwrap();
586 if pre_snippet[comment_end..].contains('\n') {
587 (
588 Some(trimmed_pre_snippet.to_owned()),
589 ListItemCommentStyle::DifferentLine,
590 )
591 } else {
592 (
593 Some(trimmed_pre_snippet.to_owned()),
594 ListItemCommentStyle::SameLine,
595 )
596 }
597 } else if starts_with_single_line_comment || starts_with_block_comment {
598 (
599 Some(trimmed_pre_snippet.to_owned()),
600 ListItemCommentStyle::DifferentLine,
601 )
602 } else {
603 (None, ListItemCommentStyle::None)
604 }
605 }
606
extract_post_comment( post_snippet: &str, comment_end: usize, separator: &str, ) -> Option<String>607 pub(crate) fn extract_post_comment(
608 post_snippet: &str,
609 comment_end: usize,
610 separator: &str,
611 ) -> Option<String> {
612 let white_space: &[_] = &[' ', '\t'];
613
614 // Cleanup post-comment: strip separators and whitespace.
615 let post_snippet = post_snippet[..comment_end].trim();
616 let post_snippet_trimmed = if post_snippet.starts_with(|c| c == ',' || c == ':') {
617 post_snippet[1..].trim_matches(white_space)
618 } else if let Some(stripped) = post_snippet.strip_prefix(separator) {
619 stripped.trim_matches(white_space)
620 }
621 // not comment or over two lines
622 else if post_snippet.ends_with(',')
623 && (!post_snippet.trim().starts_with("//") || post_snippet.trim().contains('\n'))
624 {
625 post_snippet[..(post_snippet.len() - 1)].trim_matches(white_space)
626 } else {
627 post_snippet
628 };
629 // FIXME(#3441): post_snippet includes 'const' now
630 // it should not include here
631 let removed_newline_snippet = post_snippet_trimmed.trim();
632 if !post_snippet_trimmed.is_empty()
633 && (removed_newline_snippet.starts_with("//") || removed_newline_snippet.starts_with("/*"))
634 {
635 Some(post_snippet_trimmed.to_owned())
636 } else {
637 None
638 }
639 }
640
get_comment_end( post_snippet: &str, separator: &str, terminator: &str, is_last: bool, ) -> usize641 pub(crate) fn get_comment_end(
642 post_snippet: &str,
643 separator: &str,
644 terminator: &str,
645 is_last: bool,
646 ) -> usize {
647 if is_last {
648 return post_snippet
649 .find_uncommented(terminator)
650 .unwrap_or_else(|| post_snippet.len());
651 }
652
653 let mut block_open_index = post_snippet.find("/*");
654 // check if it really is a block comment (and not `//*` or a nested comment)
655 if let Some(i) = block_open_index {
656 match post_snippet.find('/') {
657 Some(j) if j < i => block_open_index = None,
658 _ if post_snippet[..i].ends_with('/') => block_open_index = None,
659 _ => (),
660 }
661 }
662 let newline_index = post_snippet.find('\n');
663 if let Some(separator_index) = post_snippet.find_uncommented(separator) {
664 match (block_open_index, newline_index) {
665 // Separator before comment, with the next item on same line.
666 // Comment belongs to next item.
667 (Some(i), None) if i > separator_index => separator_index + 1,
668 // Block-style post-comment before the separator.
669 (Some(i), None) => cmp::max(
670 find_comment_end(&post_snippet[i..]).unwrap() + i,
671 separator_index + 1,
672 ),
673 // Block-style post-comment. Either before or after the separator.
674 (Some(i), Some(j)) if i < j => cmp::max(
675 find_comment_end(&post_snippet[i..]).unwrap() + i,
676 separator_index + 1,
677 ),
678 // Potential *single* line comment.
679 (_, Some(j)) if j > separator_index => j + 1,
680 _ => post_snippet.len(),
681 }
682 } else if let Some(newline_index) = newline_index {
683 // Match arms may not have trailing comma. In any case, for match arms,
684 // we will assume that the post comment belongs to the next arm if they
685 // do not end with trailing comma.
686 newline_index + 1
687 } else {
688 0
689 }
690 }
691
692 // Account for extra whitespace between items. This is fiddly
693 // because of the way we divide pre- and post- comments.
has_extra_newline(post_snippet: &str, comment_end: usize) -> bool694 pub(crate) fn has_extra_newline(post_snippet: &str, comment_end: usize) -> bool {
695 if post_snippet.is_empty() || comment_end == 0 {
696 return false;
697 }
698
699 let len_last = post_snippet[..comment_end]
700 .chars()
701 .last()
702 .unwrap()
703 .len_utf8();
704 // Everything from the separator to the next item.
705 let test_snippet = &post_snippet[comment_end - len_last..];
706 let first_newline = test_snippet
707 .find('\n')
708 .unwrap_or_else(|| test_snippet.len());
709 // From the end of the first line of comments.
710 let test_snippet = &test_snippet[first_newline..];
711 let first = test_snippet
712 .find(|c: char| !c.is_whitespace())
713 .unwrap_or_else(|| test_snippet.len());
714 // From the end of the first line of comments to the next non-whitespace char.
715 let test_snippet = &test_snippet[..first];
716
717 // There were multiple line breaks which got trimmed to nothing.
718 count_newlines(test_snippet) > 1
719 }
720
721 impl<'a, T, I, F1, F2, F3> Iterator for ListItems<'a, I, F1, F2, F3>
722 where
723 I: Iterator<Item = T>,
724 F1: Fn(&T) -> BytePos,
725 F2: Fn(&T) -> BytePos,
726 F3: Fn(&T) -> Option<String>,
727 {
728 type Item = ListItem;
729
next(&mut self) -> Option<Self::Item>730 fn next(&mut self) -> Option<Self::Item> {
731 self.inner.next().map(|item| {
732 // Pre-comment
733 let pre_snippet = self
734 .snippet_provider
735 .span_to_snippet(mk_sp(self.prev_span_end, (self.get_lo)(&item)))
736 .unwrap_or("");
737 let (pre_comment, pre_comment_style) = extract_pre_comment(pre_snippet);
738
739 // Post-comment
740 let next_start = match self.inner.peek() {
741 Some(next_item) => (self.get_lo)(next_item),
742 None => self.next_span_start,
743 };
744 let post_snippet = self
745 .snippet_provider
746 .span_to_snippet(mk_sp((self.get_hi)(&item), next_start))
747 .unwrap_or("");
748 let comment_end = get_comment_end(
749 post_snippet,
750 self.separator,
751 self.terminator,
752 self.inner.peek().is_none(),
753 );
754 let new_lines = has_extra_newline(post_snippet, comment_end);
755 let post_comment = extract_post_comment(post_snippet, comment_end, self.separator);
756
757 self.prev_span_end = (self.get_hi)(&item) + BytePos(comment_end as u32);
758
759 ListItem {
760 pre_comment,
761 pre_comment_style,
762 item: if self.inner.peek().is_none() && self.leave_last {
763 None
764 } else {
765 (self.get_item_string)(&item)
766 },
767 post_comment,
768 new_lines,
769 }
770 })
771 }
772 }
773
774 #[allow(clippy::too_many_arguments)]
775 // Creates an iterator over a list's items with associated comments.
itemize_list<'a, T, I, F1, F2, F3>( snippet_provider: &'a SnippetProvider, inner: I, terminator: &'a str, separator: &'a str, get_lo: F1, get_hi: F2, get_item_string: F3, prev_span_end: BytePos, next_span_start: BytePos, leave_last: bool, ) -> ListItems<'a, I, F1, F2, F3> where I: Iterator<Item = T>, F1: Fn(&T) -> BytePos, F2: Fn(&T) -> BytePos, F3: Fn(&T) -> Option<String>,776 pub(crate) fn itemize_list<'a, T, I, F1, F2, F3>(
777 snippet_provider: &'a SnippetProvider,
778 inner: I,
779 terminator: &'a str,
780 separator: &'a str,
781 get_lo: F1,
782 get_hi: F2,
783 get_item_string: F3,
784 prev_span_end: BytePos,
785 next_span_start: BytePos,
786 leave_last: bool,
787 ) -> ListItems<'a, I, F1, F2, F3>
788 where
789 I: Iterator<Item = T>,
790 F1: Fn(&T) -> BytePos,
791 F2: Fn(&T) -> BytePos,
792 F3: Fn(&T) -> Option<String>,
793 {
794 ListItems {
795 snippet_provider,
796 inner: inner.peekable(),
797 get_lo,
798 get_hi,
799 get_item_string,
800 prev_span_end,
801 next_span_start,
802 terminator,
803 separator,
804 leave_last,
805 }
806 }
807
808 /// Returns the count and total width of the list items.
calculate_width<I, T>(items: I) -> (usize, usize) where I: IntoIterator<Item = T>, T: AsRef<ListItem>,809 fn calculate_width<I, T>(items: I) -> (usize, usize)
810 where
811 I: IntoIterator<Item = T>,
812 T: AsRef<ListItem>,
813 {
814 items
815 .into_iter()
816 .map(|item| total_item_width(item.as_ref()))
817 .fold((0, 0), |acc, l| (acc.0 + 1, acc.1 + l))
818 }
819
total_item_width(item: &ListItem) -> usize820 pub(crate) fn total_item_width(item: &ListItem) -> usize {
821 comment_len(item.pre_comment.as_ref().map(|x| &(*x)[..]))
822 + comment_len(item.post_comment.as_ref().map(|x| &(*x)[..]))
823 + item.item.as_ref().map_or(0, |s| unicode_str_width(s))
824 }
825
comment_len(comment: Option<&str>) -> usize826 fn comment_len(comment: Option<&str>) -> usize {
827 match comment {
828 Some(s) => {
829 let text_len = s.trim().len();
830 if text_len > 0 {
831 // We'll put " /*" before and " */" after inline comments.
832 text_len + 6
833 } else {
834 text_len
835 }
836 }
837 None => 0,
838 }
839 }
840
841 // Compute horizontal and vertical shapes for a struct-lit-like thing.
struct_lit_shape( shape: Shape, context: &RewriteContext<'_>, prefix_width: usize, suffix_width: usize, ) -> Option<(Option<Shape>, Shape)>842 pub(crate) fn struct_lit_shape(
843 shape: Shape,
844 context: &RewriteContext<'_>,
845 prefix_width: usize,
846 suffix_width: usize,
847 ) -> Option<(Option<Shape>, Shape)> {
848 let v_shape = match context.config.indent_style() {
849 IndentStyle::Visual => shape
850 .visual_indent(0)
851 .shrink_left(prefix_width)?
852 .sub_width(suffix_width)?,
853 IndentStyle::Block => {
854 let shape = shape.block_indent(context.config.tab_spaces());
855 Shape {
856 width: context.budget(shape.indent.width()),
857 ..shape
858 }
859 }
860 };
861 let shape_width = shape.width.checked_sub(prefix_width + suffix_width);
862 if let Some(w) = shape_width {
863 let shape_width = cmp::min(w, context.config.struct_lit_width());
864 Some((Some(Shape::legacy(shape_width, shape.indent)), v_shape))
865 } else {
866 Some((None, v_shape))
867 }
868 }
869
870 // Compute the tactic for the internals of a struct-lit-like thing.
struct_lit_tactic( h_shape: Option<Shape>, context: &RewriteContext<'_>, items: &[ListItem], ) -> DefinitiveListTactic871 pub(crate) fn struct_lit_tactic(
872 h_shape: Option<Shape>,
873 context: &RewriteContext<'_>,
874 items: &[ListItem],
875 ) -> DefinitiveListTactic {
876 if let Some(h_shape) = h_shape {
877 let prelim_tactic = match (context.config.indent_style(), items.len()) {
878 (IndentStyle::Visual, 1) => ListTactic::HorizontalVertical,
879 _ if context.config.struct_lit_single_line() => ListTactic::HorizontalVertical,
880 _ => ListTactic::Vertical,
881 };
882 definitive_tactic(items, prelim_tactic, Separator::Comma, h_shape.width)
883 } else {
884 DefinitiveListTactic::Vertical
885 }
886 }
887
888 // Given a tactic and possible shapes for horizontal and vertical layout,
889 // come up with the actual shape to use.
shape_for_tactic( tactic: DefinitiveListTactic, h_shape: Option<Shape>, v_shape: Shape, ) -> Shape890 pub(crate) fn shape_for_tactic(
891 tactic: DefinitiveListTactic,
892 h_shape: Option<Shape>,
893 v_shape: Shape,
894 ) -> Shape {
895 match tactic {
896 DefinitiveListTactic::Horizontal => h_shape.unwrap(),
897 _ => v_shape,
898 }
899 }
900
901 // Create a ListFormatting object for formatting the internals of a
902 // struct-lit-like thing, that is a series of fields.
struct_lit_formatting<'a>( shape: Shape, tactic: DefinitiveListTactic, context: &'a RewriteContext<'_>, force_no_trailing_comma: bool, ) -> ListFormatting<'a>903 pub(crate) fn struct_lit_formatting<'a>(
904 shape: Shape,
905 tactic: DefinitiveListTactic,
906 context: &'a RewriteContext<'_>,
907 force_no_trailing_comma: bool,
908 ) -> ListFormatting<'a> {
909 let ends_with_newline = context.config.indent_style() != IndentStyle::Visual
910 && tactic == DefinitiveListTactic::Vertical;
911 ListFormatting {
912 tactic,
913 separator: ",",
914 trailing_separator: if force_no_trailing_comma {
915 SeparatorTactic::Never
916 } else {
917 context.config.trailing_comma()
918 },
919 separator_place: SeparatorPlace::Back,
920 shape,
921 ends_with_newline,
922 preserve_newline: true,
923 nested: false,
924 align_comments: true,
925 config: context.config,
926 }
927 }
928