| //! Format list-like expressions and items. |
| |
| use std::cmp; |
| use std::iter::Peekable; |
| |
| use syntax::source_map::BytePos; |
| |
| use crate::comment::{find_comment_end, is_last_comment_block, 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, |
| /// The decision of putting an item on a newline is determined by the caller. |
| custom_list_tactic: Vec<bool>, |
| /// Whether whitespaces should be added around the separator. |
| padding: bool, |
| } |
| |
| 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, |
| custom_list_tactic: Vec::new(), |
| padding: true, |
| } |
| } |
| |
| pub(crate) fn padding(mut self, padding: bool) -> Self { |
| self.padding = padding; |
| self |
| } |
| |
| pub(crate) fn custom_list_tactic(mut self, custom_list_tactic: Vec<bool>) -> Self { |
| self.custom_list_tactic = custom_list_tactic; |
| self |
| } |
| |
| 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<ListItem> 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<String>, |
| pub(crate) pre_comment_style: ListItemCommentStyle, |
| // Item should include attributes and doc comments. None indicates a failed |
| // rewrite. |
| pub(crate) item: Option<String>, |
| pub(crate) post_comment: Option<String>, |
| // 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: Into<String>>(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<String>) -> 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<I, T>( |
| items: I, |
| tactic: ListTactic, |
| sep: Separator, |
| width: usize, |
| ) -> DefinitiveListTactic |
| where |
| I: IntoIterator<Item = T> + Clone, |
| T: AsRef<ListItem>, |
| { |
| 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, |
| } |
| } |
| } |
| |
| /// Stores the state of the ongoing stringification of a list item. |
| struct WriteListItemState { |
| /// The offset of the item in the list. |
| ith: usize, |
| /// Whether or not the item is the first in the list. |
| first: bool, |
| /// Whether or not the item is the last in the list. |
| last: bool, |
| /// Whether or not the item is the first to be written on the current line. |
| first_item_on_line: bool, |
| /// The placement of the separator |
| sep_place: SeparatorPlace, |
| /// Whether or not the separator should be written. |
| separate: bool, |
| /// Whether or not the separator can be trailing. |
| trailing_separator: bool, |
| } |
| |
| impl WriteListItemState { |
| fn new(formatting: &ListFormatting<'_>) -> WriteListItemState { |
| WriteListItemState { |
| ith: 0, |
| first: false, |
| last: false, |
| first_item_on_line: true, |
| sep_place: SeparatorPlace::from_tactic( |
| formatting.separator_place, |
| formatting.tactic, |
| formatting.separator, |
| ), |
| separate: false, |
| // Now that we know how we will layout, we can decide for sure if there |
| // will be a trailing separator. |
| trailing_separator: formatting.needs_trailing_separator(), |
| } |
| } |
| |
| /// Set if a separator should be written for the current item. |
| fn should_separate(&mut self) { |
| self.separate = match self.sep_place { |
| SeparatorPlace::Front => !self.first, |
| SeparatorPlace::Back => !self.last || self.trailing_separator, |
| }; |
| } |
| |
| /// Returns true if a separtor should be placed in front of the current item. |
| fn should_separate_if_front(&self) -> bool { |
| self.separate && self.sep_place.is_front() |
| } |
| |
| /// Returns true if a separtor should be placed at the back of the current item. |
| fn should_separate_if_back(&self) -> bool { |
| self.separate && self.sep_place.is_back() |
| } |
| } |
| |
| /// Align comments written after items. |
| struct PostCommentAlignment { |
| /// The largest item width in a group of items. |
| item_max_width: Option<usize>, |
| /// The length of the first item. |
| first_item_len: usize, |
| /// Whether or not an item that is not the first has the same length as the first item. |
| middle_item_same_len_first: bool, |
| /// The length of the last item. |
| last_item_len: usize, |
| /// Whether or not an item that is not the last has the same length as the last item. |
| middle_item_same_len_last: bool, |
| } |
| |
| impl PostCommentAlignment { |
| fn new<T: AsRef<ListItem>>(items: &[T]) -> PostCommentAlignment { |
| let first_item_len = if let Some(item) = items.iter().nth(0) { |
| item.as_ref().inner_as_ref().len() |
| } else { |
| 0 |
| }; |
| let last_item_len = if let Some(item) = items.iter().last() { |
| item.as_ref().inner_as_ref().len() |
| } else { |
| 0 |
| }; |
| let middle_item_same_len_first = items |
| .iter() |
| // all but first item |
| .skip(1) |
| // check if any of the intermediate items share the size of the first item |
| .map(|item| item.as_ref().inner_as_ref().len()) |
| .any(|len| len == first_item_len); |
| let middle_item_same_len_last = items |
| .iter() |
| // all but last item |
| .take(items.len().saturating_sub(1)) |
| // check if any of the intermediate items share the size of the last item |
| .map(|item| item.as_ref().inner_as_ref().len()) |
| .any(|len| len == last_item_len); |
| PostCommentAlignment { |
| item_max_width: None, |
| first_item_len, |
| middle_item_same_len_first, |
| last_item_len, |
| middle_item_same_len_last, |
| } |
| } |
| |
| /// Get the length of the largest item starting at the ith-item. |
| fn update_item_max_width<T: AsRef<ListItem>>( |
| &mut self, |
| items: &[T], |
| formatting: &ListFormatting<'_>, |
| item_state: &WriteListItemState, |
| inner_item: &str, |
| overhead: usize, |
| ) { |
| if self.item_max_width.is_none() && !item_state.last && !inner_item.contains('\n') { |
| self.item_max_width = Some(PostCommentAlignment::max_width_of_item_with_post_comment( |
| &items, |
| item_state.ith, |
| overhead, |
| formatting.config.max_width(), |
| )); |
| } |
| } |
| |
| fn max_width_of_item_with_post_comment<T>( |
| items: &[T], |
| i: usize, |
| overhead: usize, |
| max_budget: usize, |
| ) -> usize |
| where |
| T: AsRef<ListItem>, |
| { |
| let mut max_width = 0; |
| let mut first = true; |
| for item in items.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 |
| } |
| |
| /// Computes the number whitespaces to insert after an item so that its post comment is aligned |
| /// with others. |
| /// |
| /// The alignment logic is complicated by the fact that the alignment is based on the longest |
| /// item, without the additional space taken by the separator. For the first and last items |
| /// where the separator may be missing, some compensation needs to be computed. |
| fn alignment( |
| &self, |
| item_state: &WriteListItemState, |
| inner_item_len: usize, |
| fmt: &ListFormatting<'_>, |
| ) -> usize { |
| // 1 = whitespace before the post_comment |
| if self.item_max_width.is_none() { |
| return 1; |
| } |
| let item_max_width = self.item_max_width.unwrap(); |
| let alignment = item_max_width.saturating_sub(inner_item_len) + 1; |
| let first_item_longest = item_max_width == self.first_item_len; |
| let last_item_longest = item_max_width == self.last_item_len; |
| |
| let alignment = match item_state.sep_place { |
| /* |
| * Front separator: first item is missing the separator and needs to be compensated |
| */ |
| SeparatorPlace::Front => { |
| match (item_state.first, first_item_longest) { |
| // in case a middle item has the same length as the first, then all items have |
| // the correct alignment: only the first item needs to account for the separator |
| _ if self.middle_item_same_len_first => { |
| alignment |
| + if item_state.first { |
| fmt.separator.len() |
| } else { |
| 0 |
| } |
| } |
| // first item is the longest: others need to minus the (separator + padding) |
| // to the alignment |
| (false, true) => alignment |
| .saturating_sub(if item_state.first_item_on_line { |
| fmt.separator.trim_start().len() |
| } else { |
| fmt.separator.len() |
| }) |
| .saturating_sub(if fmt.padding { 1 } else { 0 }), |
| // first item is not the longest: first needs to add (searator + padding) |
| // to the alignment |
| (true, false) => { |
| alignment |
| + fmt.separator.trim_start().len() |
| + if fmt.padding { 1 } else { 0 } |
| } |
| _ => alignment, |
| } |
| } |
| /* |
| * Back separator: last item is missing the separator (if it is not trailing) |
| * and needs to be compensated |
| */ |
| SeparatorPlace::Back => { |
| match (item_state.last, last_item_longest) { |
| _ if self.middle_item_same_len_last => { |
| alignment |
| + if item_state.last && !item_state.separate { |
| fmt.separator.len() |
| } else { |
| 0 |
| } |
| } |
| // last item is the longest: others need to minus the sep to the alignment |
| (false, true) if !fmt.needs_trailing_separator() => { |
| alignment.saturating_sub(fmt.separator.len()) |
| } |
| // last item is not the longest: last needs to add sep to the alignment |
| (true, false) if !item_state.separate => alignment + fmt.separator.len(), |
| _ => alignment, |
| } |
| } |
| }; |
| // at least 1 for the whitespace before the post_comment |
| if alignment == 0 { 1 } else { alignment } |
| } |
| } |
| |
| /// Format a list of commented items into a string. |
| pub(crate) fn write_list<T>(items: &[T], formatting: &ListFormatting<'_>) -> Option<String> |
| where |
| T: AsRef<ListItem>, |
| { |
| let tactic = formatting.tactic; |
| let indent_str = &formatting.shape.indent.to_string(formatting.config); |
| |
| let mut result = String::with_capacity(128); |
| let mut iter = items.iter().enumerate().peekable(); |
| |
| // Mixed tactic state |
| let mut prev_item_had_post_comment = false; |
| let mut prev_item_is_nested_import = false; |
| let mut line_len = 0; |
| |
| let mut item_state = WriteListItemState::new(formatting); |
| let mut pca = PostCommentAlignment::new(items); |
| |
| while let Some((i, item)) = iter.next() { |
| let item = item.as_ref(); |
| let inner_item = item.item.as_ref()?; |
| |
| if !item.is_substantial() { |
| continue; |
| } |
| |
| item_state.ith = i; |
| item_state.first = i == 0; |
| item_state.last = iter.peek().is_none(); |
| item_state.should_separate(); |
| let item_sep_len = if item_state.separate { |
| formatting.separator.len() |
| } else { |
| 0 |
| }; |
| match tactic { |
| _ if !formatting.custom_list_tactic.is_empty() => { |
| if *formatting |
| .custom_list_tactic |
| .get(i) |
| .expect("invalid custom_list_tactic formatting option") |
| { |
| result.push('\n'); |
| result.push_str(indent_str); |
| item_state.first_item_on_line = true; |
| } else if formatting.padding && !item_state.first_item_on_line { |
| result.push(' '); |
| } |
| } |
| DefinitiveListTactic::Horizontal if !item_state.first && formatting.padding => { |
| 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 !item_state.first && !inner_item.is_empty() && !result.is_empty() => |
| { |
| item_state.first_item_on_line = true; |
| 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 |
| || (!item_state.first && inner_item.contains("::")))) |
| { |
| result.push('\n'); |
| result.push_str(indent_str); |
| line_len = 0; |
| item_state.first_item_on_line = true; |
| if formatting.ends_with_newline { |
| item_state.trailing_separator = true; |
| } |
| } else if formatting.padding && line_len > 0 { |
| result.push(' '); |
| line_len += 1; |
| } |
| |
| if item_state.last && formatting.ends_with_newline { |
| item_state.separate = formatting.trailing_separator != SeparatorTactic::Never; |
| } |
| |
| line_len += total_width; |
| prev_item_had_post_comment = item.post_comment.is_some(); |
| prev_item_is_nested_import = inner_item.contains("::"); |
| } |
| _ => {} |
| } |
| |
| // 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(' '); |
| } |
| } |
| pca.item_max_width = None; |
| } |
| |
| if item_state.should_separate_if_front() && !item_state.first { |
| if formatting.padding { |
| result.push_str(formatting.separator.trim()); |
| result.push(' '); |
| } else if item_state.first_item_on_line { |
| result.push_str(formatting.separator.trim_start()); |
| } else { |
| result.push_str(formatting.separator); |
| } |
| } |
| 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, |
| )?; |
| |
| if is_last_comment_block(&formatted_comment) { |
| result.push(' '); |
| } else { |
| result.push('\n'); |
| result.push_str(indent_str); |
| } |
| result.push_str(&formatted_comment); |
| } |
| |
| if item_state.should_separate_if_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: Option<usize>| { |
| let overhead = if starts_with_newline(comment) { |
| 0 |
| } else if let Some(max_width) = item_max_width { |
| max_width + 2 |
| } else { |
| // 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(); |
| } |
| |
| // 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 && item_state.last |
| || comment.trim().contains('\n') |
| || comment.trim().len() > width; |
| |
| rewrite_comment( |
| comment.trim_start(), |
| block_style, |
| comment_shape, |
| formatting.config, |
| ) |
| }; |
| |
| pca.update_item_max_width(items, formatting, &item_state, inner_item, overhead); |
| let mut formatted_comment = rewrite_post_comment(pca.item_max_width)?; |
| |
| if !starts_with_newline(comment) { |
| if formatting.align_comments { |
| let mut comment_alignment = |
| pca.alignment(&item_state, inner_item.len(), formatting); |
| if first_line_width(&formatted_comment) |
| + last_line_width(&result) |
| + comment_alignment |
| > formatting.config.max_width() |
| { |
| pca.item_max_width = None; |
| pca.update_item_max_width( |
| items, |
| formatting, |
| &item_state, |
| inner_item, |
| overhead, |
| ); |
| formatted_comment = rewrite_post_comment(pca.item_max_width)?; |
| comment_alignment = |
| pca.alignment(&item_state, inner_item.len(), formatting); |
| } |
| result.push_str(&" ".repeat(comment_alignment)); |
| } else { |
| result.push(' '); |
| } |
| } else { |
| result.push('\n'); |
| result.push_str(indent_str); |
| } |
| if formatted_comment.contains('\n') { |
| pca.item_max_width = None; |
| } |
| result.push_str(&formatted_comment); |
| } else { |
| pca.item_max_width = None; |
| } |
| |
| if formatting.preserve_newline |
| && !item_state.last |
| && tactic == DefinitiveListTactic::Vertical |
| && item.new_lines |
| { |
| pca.item_max_width = None; |
| result.push('\n'); |
| } |
| |
| item_state.first_item_on_line = false; |
| } |
| |
| Some(result) |
| } |
| |
| pub(crate) struct ListItems<'a, I, F1, F2, F3> |
| where |
| I: Iterator, |
| { |
| snippet_provider: &'a SnippetProvider<'a>, |
| inner: Peekable<I>, |
| 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<String>, ListItemCommentStyle) { |
| let trimmed_pre_snippet = pre_snippet.trim(); |
| let has_block_comment = trimmed_pre_snippet.ends_with("*/"); |
| let has_single_line_comment = trimmed_pre_snippet.starts_with("//"); |
| if has_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 has_single_line_comment { |
| ( |
| Some(trimmed_pre_snippet.to_owned()), |
| ListItemCommentStyle::DifferentLine, |
| ) |
| } else { |
| (None, ListItemCommentStyle::None) |
| } |
| } |
| |
| fn extract_post_comment(post_snippet: &str, comment_end: usize, separator: &str) -> Option<String> { |
| // leading newlines are important but not when they are trailing |
| 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) |
| .trim_end_matches('\n') |
| } else if post_snippet.ends_with(separator) { |
| // the separator is in front of the next item |
| post_snippet[..post_snippet.len() - separator.len()] |
| .trim_matches(white_space) |
| .trim_end_matches('\n') |
| } else if post_snippet.starts_with(separator) { |
| post_snippet[separator.len()..] |
| .trim_matches(white_space) |
| .trim_end_matches('\n') |
| } else if post_snippet.ends_with(',') && !post_snippet.trim_start().starts_with("//") { |
| post_snippet[..post_snippet.len() - 1] |
| .trim_matches(white_space) |
| .trim_end_matches('\n') |
| } 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 |
| } |
| } |
| |
| 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. |
| 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<Item = T>, |
| F1: Fn(&T) -> BytePos, |
| F2: Fn(&T) -> BytePos, |
| F3: Fn(&T) -> Option<String>, |
| { |
| type Item = ListItem; |
| |
| fn next(&mut self) -> Option<Self::Item> { |
| 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. |
| /// |
| /// - inner is the iterator over items |
| /// - terminator is a string that closes the list, used to get comments after the last item |
| /// - separator is a string that separates the items |
| /// - get_lo is a closure to get the lower bound of an item's span |
| /// - get_hi is a closure to get the upper bound of an item's span |
| /// - get_item_string is a closure to get the rewritten item as a string |
| /// - prev_span_end is the BytePos before the first item |
| /// - next_span_start is the BytePos after the last item |
| /// - leave_last is a boolean whether or not to rewrite the last item |
| 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<Item = T>, |
| F1: Fn(&T) -> BytePos, |
| F2: Fn(&T) -> BytePos, |
| F3: Fn(&T) -> Option<String>, |
| { |
| 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<I, T>(items: I) -> (usize, usize) |
| where |
| I: IntoIterator<Item = T>, |
| T: AsRef<ListItem>, |
| { |
| 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>, 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<Shape>, |
| 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<Shape>, |
| 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, |
| custom_list_tactic: Vec::new(), |
| padding: true, |
| } |
| } |
| |
| #[cfg(test)] |
| mod test { |
| use super::*; |
| use crate::config::Config; |
| use crate::shape::{Indent, Shape}; |
| |
| #[test] |
| fn post_comment_alignment() { |
| let config: Config = Default::default(); |
| |
| let data = [ |
| //separator at the front and first item is the longest |
| ( |
| { |
| let shape = Shape::legacy(config.max_width(), Indent::empty()); |
| ListFormatting::new(shape, &config) |
| .separator("+") |
| .trailing_separator(SeparatorTactic::Always) |
| .separator_place(SeparatorPlace::Front) |
| }, |
| [ |
| ("item1aaaaaaaaaaaaaaa", "// len20"), |
| ("item2aaaaa", "// len10"), |
| ("item3aaaaaaaaaa", "// len15"), |
| ], |
| r#"item1aaaaaaaaaaaaaaa // len20 |
| + item2aaaaa // len10 |
| + item3aaaaaaaaaa // len15"#, |
| ), |
| // separator at the front and second item is the longest |
| ( |
| { |
| let shape = Shape::legacy(config.max_width(), Indent::empty()); |
| ListFormatting::new(shape, &config) |
| .separator("+") |
| .trailing_separator(SeparatorTactic::Always) |
| .separator_place(SeparatorPlace::Front) |
| }, |
| [ |
| ("item1aaaaa", "// len10"), |
| ("item2aaaaaaaaaaaaaaa", "// len20"), |
| ("item3aaaaaaaaaa", "// len15"), |
| ], |
| r#"item1aaaaa // len10 |
| + item2aaaaaaaaaaaaaaa // len20 |
| + item3aaaaaaaaaa // len15"#, |
| ), |
| ( |
| { |
| let shape = Shape::legacy(config.max_width(), Indent::empty()); |
| ListFormatting::new(shape, &config) |
| .separator("+") |
| .separator_place(SeparatorPlace::Front) |
| }, |
| [ |
| ("item1aaaaaaaaaaaaaaa", "// len20"), |
| ("item2aaaaa", "// len10"), |
| ("item3aaaaaaaaaa", "// len15"), |
| ], |
| r#"item1aaaaaaaaaaaaaaa // len20 |
| + item2aaaaa // len10 |
| + item3aaaaaaaaaa // len15"#, |
| ), |
| // font separator and middle/last items are the longest |
| ( |
| { |
| let shape = Shape::legacy(config.max_width(), Indent::empty()); |
| ListFormatting::new(shape, &config) |
| .separator("+") |
| .separator_place(SeparatorPlace::Front) |
| }, |
| [ |
| ("item1aaaaa", "// len10"), |
| ("item2aaaaaaaaaaaaaaa", "// len20"), |
| ("item3aaaaaaaaaaaaaaa", "// len20"), |
| ], |
| r#"item1aaaaa // len10 |
| + item2aaaaaaaaaaaaaaa // len20 |
| + item3aaaaaaaaaaaaaaa // len20"#, |
| ), |
| // font separator and first/middle items are the longest |
| ( |
| { |
| let shape = Shape::legacy(config.max_width(), Indent::empty()); |
| ListFormatting::new(shape, &config) |
| .separator(" +") |
| .separator_place(SeparatorPlace::Front) |
| }, |
| [ |
| ("item1aaaaaaaaaaaaaaa", "// len20"), |
| ("item2aaaaaaaaaaaaaaa", "// len20"), |
| ("item3aaaaaaaaaa", "// len15"), |
| ], |
| r#"item1aaaaaaaaaaaaaaa // len20 |
| + item2aaaaaaaaaaaaaaa // len20 |
| + item3aaaaaaaaaa // len15"#, |
| ), |
| // back separator and first item is the longest |
| ( |
| { |
| let shape = Shape::legacy(config.max_width(), Indent::empty()); |
| ListFormatting::new(shape, &config) |
| .separator(" +") |
| .separator_place(SeparatorPlace::Back) |
| }, |
| [ |
| ("item1aaaaaaaaaaaaaaa", "// len20"), |
| ("item2aaaaa", "// len10"), |
| ("item3aaaaaaaaaa", "// len15"), |
| ], |
| r#"item1aaaaaaaaaaaaaaa + // len20 |
| item2aaaaa + // len10 |
| item3aaaaaaaaaa // len15"#, |
| ), |
| // back separator and last item is the longest |
| ( |
| { |
| let shape = Shape::legacy(config.max_width(), Indent::empty()); |
| ListFormatting::new(shape, &config) |
| .separator(" +") |
| .separator_place(SeparatorPlace::Back) |
| }, |
| [ |
| ("item1aaaaa", "// len10"), |
| ("item2aaaaaaaaaa", "// len15"), |
| ("item3aaaaaaaaaaaaaaa", "// len20"), |
| ], |
| r#"item1aaaaa + // len10 |
| item2aaaaaaaaaa + // len15 |
| item3aaaaaaaaaaaaaaa // len20"#, |
| ), |
| // back separator and middle item is the longest |
| ( |
| { |
| let shape = Shape::legacy(config.max_width(), Indent::empty()); |
| ListFormatting::new(shape, &config) |
| .separator(" +") |
| .separator_place(SeparatorPlace::Back) |
| }, |
| [ |
| ("item1aaaaa", "// len10"), |
| ("item2aaaaaaaaaaaaaaa", "// len20"), |
| ("item3aaaaaaaaaa", "// len15"), |
| ], |
| r#"item1aaaaa + // len10 |
| item2aaaaaaaaaaaaaaa + // len20 |
| item3aaaaaaaaaa // len15"#, |
| ), |
| // back separator and middle/last items are the longest |
| ( |
| { |
| let shape = Shape::legacy(config.max_width(), Indent::empty()); |
| ListFormatting::new(shape, &config) |
| .separator(" +") |
| .separator_place(SeparatorPlace::Back) |
| }, |
| [ |
| ("item1aaaaa", "// len10"), |
| ("item2aaaaaaaaaaaaaaa", "// len20"), |
| ("item3aaaaaaaaaaaaaaa", "// len20"), |
| ], |
| r#"item1aaaaa + // len10 |
| item2aaaaaaaaaaaaaaa + // len20 |
| item3aaaaaaaaaaaaaaa // len20"#, |
| ), |
| // back separator and first/middle items are the longest |
| ( |
| { |
| let shape = Shape::legacy(config.max_width(), Indent::empty()); |
| ListFormatting::new(shape, &config) |
| .separator(" +") |
| .separator_place(SeparatorPlace::Back) |
| }, |
| [ |
| ("item1aaaaaaaaaaaaaaa", "// len20"), |
| ("item2aaaaaaaaaaaaaaa", "// len20"), |
| ("item3aaaaaaaaaa", "// len15"), |
| ], |
| r#"item1aaaaaaaaaaaaaaa + // len20 |
| item2aaaaaaaaaaaaaaa + // len20 |
| item3aaaaaaaaaa // len15"#, |
| ), |
| ]; |
| |
| for (i, (fmt, items, expected)) in data.into_iter().enumerate() { |
| let items = items |
| .into_iter() |
| .map(|(inner_item, post_comment)| ListItem { |
| pre_comment_style: ListItemCommentStyle::SameLine, |
| pre_comment: None, |
| item: Some(inner_item.to_string()), |
| post_comment: Some(post_comment.to_string()), |
| new_lines: false, |
| }) |
| .collect::<Vec<ListItem>>(); |
| assert_eq!( |
| write_list(&items, &fmt), |
| Some(expected.to_string()), |
| "failed on item {}", |
| i |
| ); |
| } |
| } |
| |
| #[test] |
| fn test_extract_post_comment() { |
| let data = [ |
| ( |
| ", // a comment", |
| ", // a comment".len(), |
| ",", |
| "// a comment", |
| ), |
| ( |
| ": // a comment", |
| ": // a comment".len(), |
| ":", |
| "// a comment", |
| ), |
| ( |
| "// a comment\n +", |
| "// a comment\n +".len(), |
| "+", |
| "// a comment", |
| ), |
| ( |
| "+ // a comment\n ", |
| "+ // a comment\n ".len(), |
| "+", |
| "// a comment", |
| ), |
| ( |
| "/* a comment */ ,", |
| "/* a comment */ ,".len(), |
| "+", |
| "/* a comment */", |
| ), |
| ]; |
| |
| for (i, (post_snippet, comment_end, separator, expected)) in data.iter().enumerate() { |
| assert_eq!( |
| extract_post_comment(post_snippet, *comment_end, separator), |
| Some(expected.to_string()), |
| "Failed on input {}", |
| i |
| ); |
| } |
| } |
| } |