//! Format list-like expressions and items. use std::cmp; use std::iter::Peekable; use rustc_span::BytePos; use crate::comment::{find_comment_end, rewrite_comment, FindUncommented}; use crate::config::lists::*; use crate::config::{Config, IndentStyle}; use crate::rewrite::RewriteContext; use crate::shape::{Indent, Shape}; use crate::utils::{ count_newlines, first_line_width, last_line_width, mk_sp, starts_with_newline, unicode_str_width, }; use crate::visitor::SnippetProvider; pub(crate) struct ListFormatting<'a> { tactic: DefinitiveListTactic, separator: &'a str, trailing_separator: SeparatorTactic, separator_place: SeparatorPlace, shape: Shape, // Non-expressions, e.g., items, will have a new line at the end of the list. // Important for comment styles. ends_with_newline: bool, // Remove newlines between list elements for expressions. preserve_newline: bool, // Nested import lists get some special handling for the "Mixed" list type nested: bool, // Whether comments should be visually aligned. align_comments: bool, config: &'a Config, } impl<'a> ListFormatting<'a> { pub(crate) fn new(shape: Shape, config: &'a Config) -> Self { ListFormatting { tactic: DefinitiveListTactic::Vertical, separator: ",", trailing_separator: SeparatorTactic::Never, separator_place: SeparatorPlace::Back, shape, ends_with_newline: true, preserve_newline: false, nested: false, align_comments: true, config, } } pub(crate) fn tactic(mut self, tactic: DefinitiveListTactic) -> Self { self.tactic = tactic; self } pub(crate) fn separator(mut self, separator: &'a str) -> Self { self.separator = separator; self } pub(crate) fn trailing_separator(mut self, trailing_separator: SeparatorTactic) -> Self { self.trailing_separator = trailing_separator; self } pub(crate) fn separator_place(mut self, separator_place: SeparatorPlace) -> Self { self.separator_place = separator_place; self } pub(crate) fn ends_with_newline(mut self, ends_with_newline: bool) -> Self { self.ends_with_newline = ends_with_newline; self } pub(crate) fn preserve_newline(mut self, preserve_newline: bool) -> Self { self.preserve_newline = preserve_newline; self } pub(crate) fn nested(mut self, nested: bool) -> Self { self.nested = nested; self } pub(crate) fn align_comments(mut self, align_comments: bool) -> Self { self.align_comments = align_comments; self } pub(crate) fn needs_trailing_separator(&self) -> bool { match self.trailing_separator { // We always put separator in front. SeparatorTactic::Always => true, SeparatorTactic::Vertical => self.tactic == DefinitiveListTactic::Vertical, SeparatorTactic::Never => { self.tactic == DefinitiveListTactic::Vertical && self.separator_place.is_front() } } } } impl AsRef for ListItem { fn as_ref(&self) -> &ListItem { self } } #[derive(PartialEq, Eq, Debug, Copy, Clone)] pub(crate) enum ListItemCommentStyle { // Try to keep the comment on the same line with the item. SameLine, // Put the comment on the previous or the next line of the item. DifferentLine, // No comment available. None, } #[derive(Debug, Clone)] pub(crate) struct ListItem { // None for comments mean that they are not present. pub(crate) pre_comment: Option, pub(crate) pre_comment_style: ListItemCommentStyle, // Item should include attributes and doc comments. None indicates a failed // rewrite. pub(crate) item: Option, pub(crate) post_comment: Option, // Whether there is extra whitespace before this item. pub(crate) new_lines: bool, } impl ListItem { pub(crate) fn empty() -> ListItem { ListItem { pre_comment: None, pre_comment_style: ListItemCommentStyle::None, item: None, post_comment: None, new_lines: false, } } pub(crate) fn inner_as_ref(&self) -> &str { self.item.as_ref().map_or("", |s| s) } pub(crate) fn is_different_group(&self) -> bool { self.inner_as_ref().contains('\n') || self.pre_comment.is_some() || self .post_comment .as_ref() .map_or(false, |s| s.contains('\n')) } pub(crate) fn is_multiline(&self) -> bool { self.inner_as_ref().contains('\n') || self .pre_comment .as_ref() .map_or(false, |s| s.contains('\n')) || self .post_comment .as_ref() .map_or(false, |s| s.contains('\n')) } pub(crate) fn has_single_line_comment(&self) -> bool { self.pre_comment .as_ref() .map_or(false, |comment| comment.trim_start().starts_with("//")) || self .post_comment .as_ref() .map_or(false, |comment| comment.trim_start().starts_with("//")) } pub(crate) fn has_comment(&self) -> bool { self.pre_comment.is_some() || self.post_comment.is_some() } pub(crate) fn from_str>(s: S) -> ListItem { ListItem { pre_comment: None, pre_comment_style: ListItemCommentStyle::None, item: Some(s.into()), post_comment: None, new_lines: false, } } // Returns `true` if the item causes something to be written. fn is_substantial(&self) -> bool { fn empty(s: &Option) -> bool { match *s { Some(ref s) if !s.is_empty() => false, _ => true, } } !(empty(&self.pre_comment) && empty(&self.item) && empty(&self.post_comment)) } } /// The type of separator for lists. #[derive(Copy, Clone, Eq, PartialEq, Debug)] pub(crate) enum Separator { Comma, VerticalBar, } impl Separator { pub(crate) fn len(self) -> usize { match self { // 2 = `, ` Separator::Comma => 2, // 3 = ` | ` Separator::VerticalBar => 3, } } } pub(crate) fn definitive_tactic( items: I, tactic: ListTactic, sep: Separator, width: usize, ) -> DefinitiveListTactic where I: IntoIterator + Clone, T: AsRef, { let pre_line_comments = items .clone() .into_iter() .any(|item| item.as_ref().has_single_line_comment()); let limit = match tactic { _ if pre_line_comments => return DefinitiveListTactic::Vertical, ListTactic::Horizontal => return DefinitiveListTactic::Horizontal, ListTactic::Vertical => return DefinitiveListTactic::Vertical, ListTactic::LimitedHorizontalVertical(limit) => ::std::cmp::min(width, limit), ListTactic::Mixed | ListTactic::HorizontalVertical => width, }; let (sep_count, total_width) = calculate_width(items.clone()); let total_sep_len = sep.len() * sep_count.saturating_sub(1); let real_total = total_width + total_sep_len; if real_total <= limit && !items.into_iter().any(|item| item.as_ref().is_multiline()) { DefinitiveListTactic::Horizontal } else { match tactic { ListTactic::Mixed => DefinitiveListTactic::Mixed, _ => DefinitiveListTactic::Vertical, } } } // Format a list of commented items into a string. pub(crate) fn write_list(items: I, formatting: &ListFormatting<'_>) -> Option where I: IntoIterator + Clone, T: AsRef, { let tactic = formatting.tactic; let sep_len = formatting.separator.len(); // Now that we know how we will layout, we can decide for sure if there // will be a trailing separator. let mut trailing_separator = formatting.needs_trailing_separator(); let mut result = String::with_capacity(128); let cloned_items = items.clone(); let mut iter = items.into_iter().enumerate().peekable(); let mut item_max_width: Option = None; let sep_place = SeparatorPlace::from_tactic(formatting.separator_place, tactic, formatting.separator); let mut prev_item_had_post_comment = false; let mut prev_item_is_nested_import = false; let mut line_len = 0; let indent_str = &formatting.shape.indent.to_string(formatting.config); while let Some((i, item)) = iter.next() { let item = item.as_ref(); let inner_item = item.item.as_ref()?; let first = i == 0; let last = iter.peek().is_none(); let mut separate = match sep_place { SeparatorPlace::Front => !first, SeparatorPlace::Back => !last || trailing_separator, }; let item_sep_len = if separate { sep_len } else { 0 }; // Item string may be multi-line. Its length (used for block comment alignment) // should be only the length of the last line. let item_last_line = if item.is_multiline() { inner_item.lines().last().unwrap_or("") } else { inner_item.as_ref() }; let mut item_last_line_width = item_last_line.len() + item_sep_len; if item_last_line.starts_with(&**indent_str) { item_last_line_width -= indent_str.len(); } if !item.is_substantial() { continue; } match tactic { DefinitiveListTactic::Horizontal if !first => { result.push(' '); } DefinitiveListTactic::SpecialMacro(num_args_before) => { if i == 0 { // Nothing } else if i < num_args_before { result.push(' '); } else if i <= num_args_before + 1 { result.push('\n'); result.push_str(indent_str); } else { result.push(' '); } } DefinitiveListTactic::Vertical if !first && !inner_item.is_empty() && !result.is_empty() => { result.push('\n'); result.push_str(indent_str); } DefinitiveListTactic::Mixed => { let total_width = total_item_width(item) + item_sep_len; // 1 is space between separator and item. if (line_len > 0 && line_len + 1 + total_width > formatting.shape.width) || prev_item_had_post_comment || (formatting.nested && (prev_item_is_nested_import || (!first && inner_item.contains("::")))) { result.push('\n'); result.push_str(indent_str); line_len = 0; if formatting.ends_with_newline { trailing_separator = true; } } else if line_len > 0 { result.push(' '); line_len += 1; } if last && formatting.ends_with_newline { separate = formatting.trailing_separator != SeparatorTactic::Never; } line_len += total_width; } _ => {} } // Pre-comments if let Some(ref comment) = item.pre_comment { // Block style in non-vertical mode. let block_mode = tactic == DefinitiveListTactic::Horizontal; // Width restriction is only relevant in vertical mode. let comment = rewrite_comment(comment, block_mode, formatting.shape, formatting.config)?; result.push_str(&comment); if !inner_item.is_empty() { if tactic == DefinitiveListTactic::Vertical || tactic == DefinitiveListTactic::Mixed { // We cannot keep pre-comments on the same line if the comment if normalized. let keep_comment = if formatting.config.normalize_comments() || item.pre_comment_style == ListItemCommentStyle::DifferentLine { false } else { // We will try to keep the comment on the same line with the item here. // 1 = ` ` let total_width = total_item_width(item) + item_sep_len + 1; total_width <= formatting.shape.width }; if keep_comment { result.push(' '); } else { result.push('\n'); result.push_str(indent_str); // This is the width of the item (without comments). line_len = item.item.as_ref().map_or(0, |s| unicode_str_width(&s)); } } else { result.push(' '); } } item_max_width = None; } if separate && sep_place.is_front() && !first { result.push_str(formatting.separator.trim()); result.push(' '); } result.push_str(inner_item); // Post-comments if tactic == DefinitiveListTactic::Horizontal && item.post_comment.is_some() { let comment = item.post_comment.as_ref().unwrap(); let formatted_comment = rewrite_comment( comment, true, Shape::legacy(formatting.shape.width, Indent::empty()), formatting.config, )?; result.push(' '); result.push_str(&formatted_comment); } if separate && sep_place.is_back() { result.push_str(formatting.separator); } if tactic != DefinitiveListTactic::Horizontal && item.post_comment.is_some() { let comment = item.post_comment.as_ref().unwrap(); let overhead = last_line_width(&result) + first_line_width(comment.trim()); let rewrite_post_comment = |item_max_width: &mut Option| { if item_max_width.is_none() && !last && !inner_item.contains('\n') { *item_max_width = Some(max_width_of_item_with_post_comment( &cloned_items, i, overhead, formatting.config.max_width(), )); } let overhead = if starts_with_newline(comment) { 0 } else if let Some(max_width) = *item_max_width { max_width + 2 } else { // 1 = space between item and comment. item_last_line_width + 1 }; let width = formatting.shape.width.checked_sub(overhead).unwrap_or(1); let offset = formatting.shape.indent + overhead; let comment_shape = Shape::legacy(width, offset); // Use block-style only for the last item or multiline comments. let block_style = !formatting.ends_with_newline && last || comment.trim().contains('\n') || comment.trim().len() > width; rewrite_comment( comment.trim_start(), block_style, comment_shape, formatting.config, ) }; let mut formatted_comment = rewrite_post_comment(&mut item_max_width)?; if !starts_with_newline(comment) { if formatting.align_comments { let mut comment_alignment = post_comment_alignment(item_max_width, inner_item.len()); if first_line_width(&formatted_comment) + last_line_width(&result) + comment_alignment + 1 > formatting.config.max_width() { item_max_width = None; formatted_comment = rewrite_post_comment(&mut item_max_width)?; comment_alignment = post_comment_alignment(item_max_width, inner_item.len()); } for _ in 0..=comment_alignment { result.push(' '); } } // An additional space for the missing trailing separator (or // if we skipped alignment above). if !formatting.align_comments || (last && item_max_width.is_some() && !separate && !formatting.separator.is_empty()) { result.push(' '); } } else { result.push('\n'); result.push_str(indent_str); } if formatted_comment.contains('\n') { item_max_width = None; } result.push_str(&formatted_comment); } else { item_max_width = None; } if formatting.preserve_newline && !last && tactic == DefinitiveListTactic::Vertical && item.new_lines { item_max_width = None; result.push('\n'); } prev_item_had_post_comment = item.post_comment.is_some(); prev_item_is_nested_import = inner_item.contains("::"); } Some(result) } fn max_width_of_item_with_post_comment( items: &I, i: usize, overhead: usize, max_budget: usize, ) -> usize where I: IntoIterator + Clone, T: AsRef, { let mut max_width = 0; let mut first = true; for item in items.clone().into_iter().skip(i) { let item = item.as_ref(); let inner_item_width = item.inner_as_ref().len(); if !first && (item.is_different_group() || item.post_comment.is_none() || inner_item_width + overhead > max_budget) { return max_width; } if max_width < inner_item_width { max_width = inner_item_width; } if item.new_lines { return max_width; } first = false; } max_width } fn post_comment_alignment(item_max_width: Option, inner_item_len: usize) -> usize { item_max_width.unwrap_or(0).saturating_sub(inner_item_len) } pub(crate) struct ListItems<'a, I, F1, F2, F3> where I: Iterator, { snippet_provider: &'a SnippetProvider, inner: Peekable, get_lo: F1, get_hi: F2, get_item_string: F3, prev_span_end: BytePos, next_span_start: BytePos, terminator: &'a str, separator: &'a str, leave_last: bool, } pub(crate) fn extract_pre_comment(pre_snippet: &str) -> (Option, ListItemCommentStyle) { let trimmed_pre_snippet = pre_snippet.trim(); // Both start and end are checked to support keeping a block comment inline with // the item, even if there are preceeding line comments, while still supporting // a snippet that starts with a block comment but also contains one or more // trailing single line comments. // https://github.com/rust-lang/rustfmt/issues/3025 // https://github.com/rust-lang/rustfmt/pull/3048 // https://github.com/rust-lang/rustfmt/issues/3839 let starts_with_block_comment = trimmed_pre_snippet.starts_with("/*"); let ends_with_block_comment = trimmed_pre_snippet.ends_with("*/"); let starts_with_single_line_comment = trimmed_pre_snippet.starts_with("//"); if ends_with_block_comment { let comment_end = pre_snippet.rfind(|c| c == '/').unwrap(); if pre_snippet[comment_end..].contains('\n') { ( Some(trimmed_pre_snippet.to_owned()), ListItemCommentStyle::DifferentLine, ) } else { ( Some(trimmed_pre_snippet.to_owned()), ListItemCommentStyle::SameLine, ) } } else if starts_with_single_line_comment || starts_with_block_comment { ( Some(trimmed_pre_snippet.to_owned()), ListItemCommentStyle::DifferentLine, ) } else { (None, ListItemCommentStyle::None) } } pub(crate) fn extract_post_comment( post_snippet: &str, comment_end: usize, separator: &str, ) -> Option { let white_space: &[_] = &[' ', '\t']; // Cleanup post-comment: strip separators and whitespace. let post_snippet = post_snippet[..comment_end].trim(); let post_snippet_trimmed = if post_snippet.starts_with(|c| c == ',' || c == ':') { post_snippet[1..].trim_matches(white_space) } else if post_snippet.starts_with(separator) { post_snippet[separator.len()..].trim_matches(white_space) } // not comment or over two lines else if post_snippet.ends_with(',') && (!post_snippet.trim().starts_with("//") || post_snippet.trim().contains('\n')) { post_snippet[..(post_snippet.len() - 1)].trim_matches(white_space) } else { post_snippet }; // FIXME(#3441): post_snippet includes 'const' now // it should not include here let removed_newline_snippet = post_snippet_trimmed.trim(); if !post_snippet_trimmed.is_empty() && (removed_newline_snippet.starts_with("//") || removed_newline_snippet.starts_with("/*")) { Some(post_snippet_trimmed.to_owned()) } else { None } } pub(crate) fn get_comment_end( post_snippet: &str, separator: &str, terminator: &str, is_last: bool, ) -> usize { if is_last { return post_snippet .find_uncommented(terminator) .unwrap_or_else(|| post_snippet.len()); } let mut block_open_index = post_snippet.find("/*"); // check if it really is a block comment (and not `//*` or a nested comment) if let Some(i) = block_open_index { match post_snippet.find('/') { Some(j) if j < i => block_open_index = None, _ if post_snippet[..i].ends_with('/') => block_open_index = None, _ => (), } } let newline_index = post_snippet.find('\n'); if let Some(separator_index) = post_snippet.find_uncommented(separator) { match (block_open_index, newline_index) { // Separator before comment, with the next item on same line. // Comment belongs to next item. (Some(i), None) if i > separator_index => separator_index + 1, // Block-style post-comment before the separator. (Some(i), None) => cmp::max( find_comment_end(&post_snippet[i..]).unwrap() + i, separator_index + 1, ), // Block-style post-comment. Either before or after the separator. (Some(i), Some(j)) if i < j => cmp::max( find_comment_end(&post_snippet[i..]).unwrap() + i, separator_index + 1, ), // Potential *single* line comment. (_, Some(j)) if j > separator_index => j + 1, _ => post_snippet.len(), } } else if let Some(newline_index) = newline_index { // Match arms may not have trailing comma. In any case, for match arms, // we will assume that the post comment belongs to the next arm if they // do not end with trailing comma. newline_index + 1 } else { 0 } } // Account for extra whitespace between items. This is fiddly // because of the way we divide pre- and post- comments. pub(crate) fn has_extra_newline(post_snippet: &str, comment_end: usize) -> bool { if post_snippet.is_empty() || comment_end == 0 { return false; } let len_last = post_snippet[..comment_end] .chars() .last() .unwrap() .len_utf8(); // Everything from the separator to the next item. let test_snippet = &post_snippet[comment_end - len_last..]; let first_newline = test_snippet .find('\n') .unwrap_or_else(|| test_snippet.len()); // From the end of the first line of comments. let test_snippet = &test_snippet[first_newline..]; let first = test_snippet .find(|c: char| !c.is_whitespace()) .unwrap_or_else(|| test_snippet.len()); // From the end of the first line of comments to the next non-whitespace char. let test_snippet = &test_snippet[..first]; // There were multiple line breaks which got trimmed to nothing. count_newlines(test_snippet) > 1 } impl<'a, T, I, F1, F2, F3> Iterator for ListItems<'a, I, F1, F2, F3> where I: Iterator, F1: Fn(&T) -> BytePos, F2: Fn(&T) -> BytePos, F3: Fn(&T) -> Option, { type Item = ListItem; fn next(&mut self) -> Option { self.inner.next().map(|item| { // Pre-comment let pre_snippet = self .snippet_provider .span_to_snippet(mk_sp(self.prev_span_end, (self.get_lo)(&item))) .unwrap_or(""); let (pre_comment, pre_comment_style) = extract_pre_comment(pre_snippet); // Post-comment let next_start = match self.inner.peek() { Some(next_item) => (self.get_lo)(next_item), None => self.next_span_start, }; let post_snippet = self .snippet_provider .span_to_snippet(mk_sp((self.get_hi)(&item), next_start)) .unwrap_or(""); let comment_end = get_comment_end( post_snippet, self.separator, self.terminator, self.inner.peek().is_none(), ); let new_lines = has_extra_newline(post_snippet, comment_end); let post_comment = extract_post_comment(post_snippet, comment_end, self.separator); self.prev_span_end = (self.get_hi)(&item) + BytePos(comment_end as u32); ListItem { pre_comment, pre_comment_style, item: if self.inner.peek().is_none() && self.leave_last { None } else { (self.get_item_string)(&item) }, post_comment, new_lines, } }) } } #[allow(clippy::too_many_arguments)] // Creates an iterator over a list's items with associated comments. pub(crate) fn itemize_list<'a, T, I, F1, F2, F3>( snippet_provider: &'a SnippetProvider, inner: I, terminator: &'a str, separator: &'a str, get_lo: F1, get_hi: F2, get_item_string: F3, prev_span_end: BytePos, next_span_start: BytePos, leave_last: bool, ) -> ListItems<'a, I, F1, F2, F3> where I: Iterator, F1: Fn(&T) -> BytePos, F2: Fn(&T) -> BytePos, F3: Fn(&T) -> Option, { ListItems { snippet_provider, inner: inner.peekable(), get_lo, get_hi, get_item_string, prev_span_end, next_span_start, terminator, separator, leave_last, } } /// Returns the count and total width of the list items. fn calculate_width(items: I) -> (usize, usize) where I: IntoIterator, T: AsRef, { items .into_iter() .map(|item| total_item_width(item.as_ref())) .fold((0, 0), |acc, l| (acc.0 + 1, acc.1 + l)) } pub(crate) fn total_item_width(item: &ListItem) -> usize { comment_len(item.pre_comment.as_ref().map(|x| &(*x)[..])) + comment_len(item.post_comment.as_ref().map(|x| &(*x)[..])) + &item.item.as_ref().map_or(0, |s| unicode_str_width(&s)) } fn comment_len(comment: Option<&str>) -> usize { match comment { Some(s) => { let text_len = s.trim().len(); if text_len > 0 { // We'll put " /*" before and " */" after inline comments. text_len + 6 } else { text_len } } None => 0, } } // Compute horizontal and vertical shapes for a struct-lit-like thing. pub(crate) fn struct_lit_shape( shape: Shape, context: &RewriteContext<'_>, prefix_width: usize, suffix_width: usize, ) -> Option<(Option, Shape)> { let v_shape = match context.config.indent_style() { IndentStyle::Visual => shape .visual_indent(0) .shrink_left(prefix_width)? .sub_width(suffix_width)?, IndentStyle::Block => { let shape = shape.block_indent(context.config.tab_spaces()); Shape { width: context.budget(shape.indent.width()), ..shape } } }; let shape_width = shape.width.checked_sub(prefix_width + suffix_width); if let Some(w) = shape_width { let shape_width = cmp::min(w, context.config.width_heuristics().struct_lit_width); Some((Some(Shape::legacy(shape_width, shape.indent)), v_shape)) } else { Some((None, v_shape)) } } // Compute the tactic for the internals of a struct-lit-like thing. pub(crate) fn struct_lit_tactic( h_shape: Option, context: &RewriteContext<'_>, items: &[ListItem], ) -> DefinitiveListTactic { if let Some(h_shape) = h_shape { let prelim_tactic = match (context.config.indent_style(), items.len()) { (IndentStyle::Visual, 1) => ListTactic::HorizontalVertical, _ if context.config.struct_lit_single_line() => ListTactic::HorizontalVertical, _ => ListTactic::Vertical, }; definitive_tactic(items, prelim_tactic, Separator::Comma, h_shape.width) } else { DefinitiveListTactic::Vertical } } // Given a tactic and possible shapes for horizontal and vertical layout, // come up with the actual shape to use. pub(crate) fn shape_for_tactic( tactic: DefinitiveListTactic, h_shape: Option, v_shape: Shape, ) -> Shape { match tactic { DefinitiveListTactic::Horizontal => h_shape.unwrap(), _ => v_shape, } } // Create a ListFormatting object for formatting the internals of a // struct-lit-like thing, that is a series of fields. pub(crate) fn struct_lit_formatting<'a>( shape: Shape, tactic: DefinitiveListTactic, context: &'a RewriteContext<'_>, force_no_trailing_comma: bool, ) -> ListFormatting<'a> { let ends_with_newline = context.config.indent_style() != IndentStyle::Visual && tactic == DefinitiveListTactic::Vertical; ListFormatting { tactic, separator: ",", trailing_separator: if force_no_trailing_comma { SeparatorTactic::Never } else { context.config.trailing_comma() }, separator_place: SeparatorPlace::Back, shape, ends_with_newline, preserve_newline: true, nested: false, align_comments: true, config: context.config, } }