Skip to main content

rustfmt_nightly/
lists.rs

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