rustfmt_nightly/
lists.rs

1//! Format list-like expressions and items.
2
3use std::cmp;
4use std::iter::Peekable;
5
6use rustc_span::BytePos;
7
8use crate::comment::{FindUncommented, find_comment_end, rewrite_comment};
9use crate::config::lists::*;
10use crate::config::{Config, IndentStyle};
11use crate::rewrite::{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) + first_line_width(comment.trim());
429
430            let rewrite_post_comment = |item_max_width: &mut Option<usize>| {
431                if item_max_width.is_none() && !last && !inner_item.contains('\n') {
432                    *item_max_width = Some(max_width_of_item_with_post_comment(
433                        &cloned_items,
434                        i,
435                        overhead,
436                        formatting.config.max_width(),
437                    ));
438                }
439                let overhead = if starts_with_newline(comment) {
440                    0
441                } else if let Some(max_width) = *item_max_width {
442                    max_width + 2
443                } else {
444                    // 1 = space between item and comment.
445                    item_last_line_width + 1
446                };
447                let width = formatting.shape.width.checked_sub(overhead).unwrap_or(1);
448                let offset = formatting.shape.indent + overhead;
449                let comment_shape = Shape::legacy(width, offset);
450
451                let block_style = if !formatting.ends_with_newline && last {
452                    true
453                } else if starts_with_newline(comment) {
454                    false
455                } else {
456                    comment.trim().contains('\n') || unicode_str_width(comment.trim()) > width
457                };
458
459                rewrite_comment(
460                    comment.trim_start(),
461                    block_style,
462                    comment_shape,
463                    formatting.config,
464                )
465            };
466
467            let mut formatted_comment = rewrite_post_comment(&mut item_max_width)?;
468
469            if !starts_with_newline(comment) {
470                if formatting.align_comments {
471                    let mut comment_alignment =
472                        post_comment_alignment(item_max_width, unicode_str_width(inner_item));
473                    if first_line_width(&formatted_comment)
474                        + last_line_width(&result)
475                        + comment_alignment
476                        + 1
477                        > formatting.config.max_width()
478                    {
479                        item_max_width = None;
480                        formatted_comment = rewrite_post_comment(&mut item_max_width)?;
481                        comment_alignment =
482                            post_comment_alignment(item_max_width, unicode_str_width(inner_item));
483                    }
484                    for _ in 0..=comment_alignment {
485                        result.push(' ');
486                    }
487                }
488                // An additional space for the missing trailing separator (or
489                // if we skipped alignment above).
490                if !formatting.align_comments
491                    || (last
492                        && item_max_width.is_some()
493                        && !separate
494                        && !formatting.separator.is_empty())
495                {
496                    result.push(' ');
497                }
498            } else {
499                result.push('\n');
500                result.push_str(indent_str);
501            }
502            if formatted_comment.contains('\n') {
503                item_max_width = None;
504            }
505            result.push_str(&formatted_comment);
506        } else {
507            item_max_width = None;
508        }
509
510        if formatting.preserve_newline
511            && !last
512            && tactic == DefinitiveListTactic::Vertical
513            && item.new_lines
514        {
515            item_max_width = None;
516            result.push('\n');
517        }
518
519        prev_item_had_post_comment = item.post_comment.is_some();
520        prev_item_is_nested_import = inner_item.contains("::");
521    }
522
523    Ok(result)
524}
525
526fn max_width_of_item_with_post_comment<I, T>(
527    items: &I,
528    i: usize,
529    overhead: usize,
530    max_budget: usize,
531) -> usize
532where
533    I: IntoIterator<Item = T> + Clone,
534    T: AsRef<ListItem>,
535{
536    let mut max_width = 0;
537    let mut first = true;
538    for item in items.clone().into_iter().skip(i) {
539        let item = item.as_ref();
540        let inner_item_width = unicode_str_width(item.inner_as_ref());
541        if !first
542            && (item.is_different_group()
543                || item.post_comment.is_none()
544                || inner_item_width + overhead > max_budget)
545        {
546            return max_width;
547        }
548        if max_width < inner_item_width {
549            max_width = inner_item_width;
550        }
551        if item.new_lines {
552            return max_width;
553        }
554        first = false;
555    }
556    max_width
557}
558
559fn post_comment_alignment(item_max_width: Option<usize>, inner_item_width: usize) -> usize {
560    item_max_width.unwrap_or(0).saturating_sub(inner_item_width)
561}
562
563pub(crate) struct ListItems<'a, I, F1, F2, F3>
564where
565    I: Iterator,
566{
567    snippet_provider: &'a SnippetProvider,
568    inner: Peekable<I>,
569    get_lo: F1,
570    get_hi: F2,
571    get_item_string: F3,
572    prev_span_end: BytePos,
573    next_span_start: BytePos,
574    terminator: &'a str,
575    separator: &'a str,
576    leave_last: bool,
577}
578
579pub(crate) fn extract_pre_comment(pre_snippet: &str) -> (Option<String>, ListItemCommentStyle) {
580    let trimmed_pre_snippet = pre_snippet.trim();
581    // Both start and end are checked to support keeping a block comment inline with
582    // the item, even if there are preceding line comments, while still supporting
583    // a snippet that starts with a block comment but also contains one or more
584    // trailing single line comments.
585    // https://github.com/rust-lang/rustfmt/issues/3025
586    // https://github.com/rust-lang/rustfmt/pull/3048
587    // https://github.com/rust-lang/rustfmt/issues/3839
588    let starts_with_block_comment = trimmed_pre_snippet.starts_with("/*");
589    let ends_with_block_comment = trimmed_pre_snippet.ends_with("*/");
590    let starts_with_single_line_comment = trimmed_pre_snippet.starts_with("//");
591    if ends_with_block_comment {
592        let comment_end = pre_snippet.rfind(|c| c == '/').unwrap();
593        if pre_snippet[comment_end..].contains('\n') {
594            (
595                Some(trimmed_pre_snippet.to_owned()),
596                ListItemCommentStyle::DifferentLine,
597            )
598        } else {
599            (
600                Some(trimmed_pre_snippet.to_owned()),
601                ListItemCommentStyle::SameLine,
602            )
603        }
604    } else if starts_with_single_line_comment || starts_with_block_comment {
605        (
606            Some(trimmed_pre_snippet.to_owned()),
607            ListItemCommentStyle::DifferentLine,
608        )
609    } else {
610        (None, ListItemCommentStyle::None)
611    }
612}
613
614pub(crate) fn extract_post_comment(
615    post_snippet: &str,
616    comment_end: usize,
617    separator: &str,
618    is_last: bool,
619) -> Option<String> {
620    let white_space: &[_] = &[' ', '\t'];
621
622    // Cleanup post-comment: strip separators and whitespace.
623    let post_snippet = post_snippet[..comment_end].trim();
624
625    let last_inline_comment_ends_with_separator = if is_last {
626        if let Some(line) = post_snippet.lines().last() {
627            line.ends_with(separator) && line.trim().starts_with("//")
628        } else {
629            false
630        }
631    } else {
632        false
633    };
634
635    let post_snippet_trimmed = if post_snippet.starts_with(|c| c == ',' || c == ':') {
636        post_snippet[1..].trim_matches(white_space)
637    } else if let Some(stripped) = post_snippet.strip_prefix(separator) {
638        stripped.trim_matches(white_space)
639    } else if last_inline_comment_ends_with_separator {
640        // since we're on the last item it's fine to keep any trailing separators in comments
641        post_snippet.trim_matches(white_space)
642    }
643    // not comment or over two lines
644    else if post_snippet.ends_with(separator)
645        && (!post_snippet.trim().starts_with("//") || post_snippet.trim().contains('\n'))
646    {
647        post_snippet[..(post_snippet.len() - 1)].trim_matches(white_space)
648    } else {
649        post_snippet
650    };
651    // FIXME(#3441): post_snippet includes 'const' now
652    // it should not include here
653    let removed_newline_snippet = post_snippet_trimmed.trim();
654    if !post_snippet_trimmed.is_empty()
655        && (removed_newline_snippet.starts_with("//") || removed_newline_snippet.starts_with("/*"))
656    {
657        Some(post_snippet_trimmed.to_owned())
658    } else {
659        None
660    }
661}
662
663pub(crate) fn get_comment_end(
664    post_snippet: &str,
665    separator: &str,
666    terminator: &str,
667    is_last: bool,
668) -> usize {
669    if is_last {
670        return post_snippet
671            .find_uncommented(terminator)
672            .unwrap_or_else(|| post_snippet.len());
673    }
674
675    let mut block_open_index = post_snippet.find("/*");
676    // check if it really is a block comment (and not `//*` or a nested comment)
677    if let Some(i) = block_open_index {
678        match post_snippet.find('/') {
679            Some(j) if j < i => block_open_index = None,
680            _ if post_snippet[..i].ends_with('/') => block_open_index = None,
681            _ => (),
682        }
683    }
684    let newline_index = post_snippet.find('\n');
685    if let Some(separator_index) = post_snippet.find_uncommented(separator) {
686        match (block_open_index, newline_index) {
687            // Separator before comment, with the next item on same line.
688            // Comment belongs to next item.
689            (Some(i), None) if i > separator_index => separator_index + 1,
690            // Block-style post-comment before the separator.
691            (Some(i), None) => cmp::max(
692                find_comment_end(&post_snippet[i..]).unwrap() + i,
693                separator_index + 1,
694            ),
695            // Block-style post-comment. Either before or after the separator.
696            (Some(i), Some(j)) if i < j => cmp::max(
697                find_comment_end(&post_snippet[i..]).unwrap() + i,
698                separator_index + 1,
699            ),
700            // Potential *single* line comment.
701            (_, Some(j)) if j > separator_index => j + 1,
702            _ => post_snippet.len(),
703        }
704    } else if let Some(newline_index) = newline_index {
705        // Match arms may not have trailing comma. In any case, for match arms,
706        // we will assume that the post comment belongs to the next arm if they
707        // do not end with trailing comma.
708        newline_index + 1
709    } else {
710        0
711    }
712}
713
714// Account for extra whitespace between items. This is fiddly
715// because of the way we divide pre- and post- comments.
716pub(crate) fn has_extra_newline(post_snippet: &str, comment_end: usize) -> bool {
717    if post_snippet.is_empty() || comment_end == 0 {
718        return false;
719    }
720
721    let len_last = post_snippet[..comment_end]
722        .chars()
723        .last()
724        .unwrap()
725        .len_utf8();
726    // Everything from the separator to the next item.
727    let test_snippet = &post_snippet[comment_end - len_last..];
728    let first_newline = test_snippet
729        .find('\n')
730        .unwrap_or_else(|| test_snippet.len());
731    // From the end of the first line of comments.
732    let test_snippet = &test_snippet[first_newline..];
733    let first = test_snippet
734        .find(|c: char| !c.is_whitespace())
735        .unwrap_or_else(|| test_snippet.len());
736    // From the end of the first line of comments to the next non-whitespace char.
737    let test_snippet = &test_snippet[..first];
738
739    // There were multiple line breaks which got trimmed to nothing.
740    count_newlines(test_snippet) > 1
741}
742
743impl<'a, T, I, F1, F2, F3> Iterator for ListItems<'a, I, F1, F2, F3>
744where
745    I: Iterator<Item = T>,
746    F1: Fn(&T) -> BytePos,
747    F2: Fn(&T) -> BytePos,
748    F3: Fn(&T) -> RewriteResult,
749{
750    type Item = ListItem;
751
752    fn next(&mut self) -> Option<Self::Item> {
753        self.inner.next().map(|item| {
754            // Pre-comment
755            let pre_snippet = self
756                .snippet_provider
757                .span_to_snippet(mk_sp(self.prev_span_end, (self.get_lo)(&item)))
758                .unwrap_or("");
759            let (pre_comment, pre_comment_style) = extract_pre_comment(pre_snippet);
760
761            // Post-comment
762            let next_start = match self.inner.peek() {
763                Some(next_item) => (self.get_lo)(next_item),
764                None => self.next_span_start,
765            };
766            let post_snippet = self
767                .snippet_provider
768                .span_to_snippet(mk_sp((self.get_hi)(&item), next_start))
769                .unwrap_or("");
770            let is_last = self.inner.peek().is_none();
771            let comment_end =
772                get_comment_end(post_snippet, self.separator, self.terminator, is_last);
773            let new_lines = has_extra_newline(post_snippet, comment_end);
774            let post_comment =
775                extract_post_comment(post_snippet, comment_end, self.separator, is_last);
776
777            self.prev_span_end = (self.get_hi)(&item) + BytePos(comment_end as u32);
778
779            ListItem {
780                pre_comment,
781                pre_comment_style,
782                // leave_last is set to true only for rewrite_items
783                item: if self.inner.peek().is_none() && self.leave_last {
784                    Err(RewriteError::SkipFormatting)
785                } else {
786                    (self.get_item_string)(&item)
787                },
788                post_comment,
789                new_lines,
790            }
791        })
792    }
793}
794
795#[allow(clippy::too_many_arguments)]
796// Creates an iterator over a list's items with associated comments.
797pub(crate) fn itemize_list<'a, T, I, F1, F2, F3>(
798    snippet_provider: &'a SnippetProvider,
799    inner: I,
800    terminator: &'a str,
801    separator: &'a str,
802    get_lo: F1,
803    get_hi: F2,
804    get_item_string: F3,
805    prev_span_end: BytePos,
806    next_span_start: BytePos,
807    leave_last: bool,
808) -> ListItems<'a, I, F1, F2, F3>
809where
810    I: Iterator<Item = T>,
811    F1: Fn(&T) -> BytePos,
812    F2: Fn(&T) -> BytePos,
813    F3: Fn(&T) -> RewriteResult,
814{
815    ListItems {
816        snippet_provider,
817        inner: inner.peekable(),
818        get_lo,
819        get_hi,
820        get_item_string,
821        prev_span_end,
822        next_span_start,
823        terminator,
824        separator,
825        leave_last,
826    }
827}
828
829/// Returns the count and total width of the list items.
830fn calculate_width<I, T>(items: I) -> (usize, usize)
831where
832    I: IntoIterator<Item = T>,
833    T: AsRef<ListItem>,
834{
835    items
836        .into_iter()
837        .map(|item| total_item_width(item.as_ref()))
838        .fold((0, 0), |acc, l| (acc.0 + 1, acc.1 + l))
839}
840
841pub(crate) fn total_item_width(item: &ListItem) -> usize {
842    comment_len(item.pre_comment.as_ref().map(|x| &(*x)[..]))
843        + comment_len(item.post_comment.as_ref().map(|x| &(*x)[..]))
844        + item.item.as_ref().map_or(0, |s| unicode_str_width(s))
845}
846
847fn comment_len(comment: Option<&str>) -> usize {
848    match comment {
849        Some(s) => {
850            let text_len = s.trim().len();
851            if text_len > 0 {
852                // We'll put " /*" before and " */" after inline comments.
853                text_len + 6
854            } else {
855                text_len
856            }
857        }
858        None => 0,
859    }
860}
861
862// Compute horizontal and vertical shapes for a struct-lit-like thing.
863pub(crate) fn struct_lit_shape(
864    shape: Shape,
865    context: &RewriteContext<'_>,
866    prefix_width: usize,
867    suffix_width: usize,
868) -> Option<(Option<Shape>, Shape)> {
869    let v_shape = match context.config.indent_style() {
870        IndentStyle::Visual => shape
871            .visual_indent(0)
872            .shrink_left(prefix_width)?
873            .sub_width(suffix_width)?,
874        IndentStyle::Block => {
875            let shape = shape.block_indent(context.config.tab_spaces());
876            Shape {
877                width: context.budget(shape.indent.width()),
878                ..shape
879            }
880        }
881    };
882    let shape_width = shape.width.checked_sub(prefix_width + suffix_width);
883    if let Some(w) = shape_width {
884        let shape_width = cmp::min(w, context.config.struct_lit_width());
885        Some((Some(Shape::legacy(shape_width, shape.indent)), v_shape))
886    } else {
887        Some((None, v_shape))
888    }
889}
890
891// Compute the tactic for the internals of a struct-lit-like thing.
892pub(crate) fn struct_lit_tactic(
893    h_shape: Option<Shape>,
894    context: &RewriteContext<'_>,
895    items: &[ListItem],
896) -> DefinitiveListTactic {
897    if let Some(h_shape) = h_shape {
898        let prelim_tactic = match (context.config.indent_style(), items.len()) {
899            (IndentStyle::Visual, 1) => ListTactic::HorizontalVertical,
900            _ if context.config.struct_lit_single_line() => ListTactic::HorizontalVertical,
901            _ => ListTactic::Vertical,
902        };
903        definitive_tactic(items, prelim_tactic, Separator::Comma, h_shape.width)
904    } else {
905        DefinitiveListTactic::Vertical
906    }
907}
908
909// Given a tactic and possible shapes for horizontal and vertical layout,
910// come up with the actual shape to use.
911pub(crate) fn shape_for_tactic(
912    tactic: DefinitiveListTactic,
913    h_shape: Option<Shape>,
914    v_shape: Shape,
915) -> Shape {
916    match tactic {
917        DefinitiveListTactic::Horizontal => h_shape.unwrap(),
918        _ => v_shape,
919    }
920}
921
922// Create a ListFormatting object for formatting the internals of a
923// struct-lit-like thing, that is a series of fields.
924pub(crate) fn struct_lit_formatting<'a>(
925    shape: Shape,
926    tactic: DefinitiveListTactic,
927    context: &'a RewriteContext<'_>,
928    force_no_trailing_comma: bool,
929) -> ListFormatting<'a> {
930    let ends_with_newline = context.config.indent_style() != IndentStyle::Visual
931        && tactic == DefinitiveListTactic::Vertical;
932    ListFormatting {
933        tactic,
934        separator: ",",
935        trailing_separator: if force_no_trailing_comma {
936            SeparatorTactic::Never
937        } else {
938            context.config.trailing_comma()
939        },
940        separator_place: SeparatorPlace::Back,
941        shape,
942        ends_with_newline,
943        preserve_newline: true,
944        nested: false,
945        align_comments: true,
946        config: context.config,
947    }
948}