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