use std::cmp;
use std::iter::Peekable;
-use syntax::codemap::{CodeMap, BytePos};
+use syntax::codemap::{BytePos, CodeMap};
use {Indent, Shape};
use comment::{find_comment_end, rewrite_comment, FindUncommented};
use rewrite::RewriteContext;
use utils::{first_line_width, last_line_width, mk_sp};
-#[derive(Eq, PartialEq, Debug, Copy, Clone)]
/// Formatting tactic for lists. This will be cast down to a
-/// DefinitiveListTactic depending on the number and length of the items and
+/// `DefinitiveListTactic` depending on the number and length of the items and
/// their comments.
+#[derive(Eq, PartialEq, Debug, Copy, Clone)]
pub enum ListTactic {
// One item per row.
Vertical,
pub tactic: DefinitiveListTactic,
pub separator: &'a str,
pub trailing_separator: SeparatorTactic,
+ pub separator_place: SeparatorPlace,
pub shape: Shape,
// Non-expressions, e.g. items, will have a new line at the end of the list.
// Important for comment styles.
pub ends_with_newline: bool,
+ // Remove newlines between list elements for expressions.
+ pub preserve_newline: bool,
pub config: &'a Config,
}
+impl<'a> ListFormatting<'a> {
+ pub 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)]
+pub 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,
+}
+
pub struct ListItem {
// None for comments mean that they are not present.
pub pre_comment: Option<String>,
+ pub pre_comment_style: ListItemCommentStyle,
// Item should include attributes and doc comments. None indicates a failed
// rewrite.
pub item: Option<String>,
impl ListItem {
pub fn inner_as_ref(&self) -> &str {
- self.item.as_ref().map_or("", |s| &*s)
+ self.item.as_ref().map_or("", |s| s)
}
- pub fn is_multiline(&self) -> bool {
+ pub 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 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 fn has_comment(&self) -> bool {
self.pre_comment
.as_ref()
pub 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,
}
}
-#[derive(Eq, PartialEq, Debug, Copy, Clone)]
/// The definitive formatting tactic for lists.
+#[derive(Eq, PartialEq, Debug, Copy, Clone)]
pub enum DefinitiveListTactic {
Vertical,
Horizontal,
Mixed,
}
-pub fn definitive_tactic<I, T>(items: I, tactic: ListTactic, width: usize) -> DefinitiveListTactic
+impl DefinitiveListTactic {
+ pub fn ends_with_newline(&self, indent_style: IndentStyle) -> bool {
+ match indent_style {
+ IndentStyle::Block => *self != DefinitiveListTactic::Horizontal,
+ IndentStyle::Visual => false,
+ }
+ }
+}
+
+/// The type of separator for lists.
+#[derive(Copy, Clone, Eq, PartialEq, Debug)]
+pub enum Separator {
+ Comma,
+ VerticalBar,
+}
+
+impl Separator {
+ pub fn len(&self) -> usize {
+ match *self {
+ // 2 = `, `
+ Separator::Comma => 2,
+ // 3 = ` | `
+ Separator::VerticalBar => 3,
+ }
+ }
+}
+
+/// Where to put separator.
+#[derive(Eq, PartialEq, Debug, Copy, Clone)]
+pub enum SeparatorPlace {
+ Front,
+ Back,
+}
+
+impl_enum_serialize_and_deserialize!(SeparatorPlace, Front, Back);
+
+impl SeparatorPlace {
+ pub fn is_front(&self) -> bool {
+ *self == SeparatorPlace::Front
+ }
+
+ pub fn is_back(&self) -> bool {
+ *self == SeparatorPlace::Back
+ }
+
+ pub fn from_tactic(default: SeparatorPlace, tactic: DefinitiveListTactic) -> SeparatorPlace {
+ match tactic {
+ DefinitiveListTactic::Vertical => default,
+ _ => SeparatorPlace::Back,
+ }
+ }
+}
+
+pub fn definitive_tactic<I, T>(
+ items: I,
+ tactic: ListTactic,
+ sep: Separator,
+ width: usize,
+) -> DefinitiveListTactic
where
I: IntoIterator<Item = T> + Clone,
T: AsRef<ListItem>,
};
let (sep_count, total_width) = calculate_width(items.clone());
- let sep_len = ", ".len(); // FIXME: make more generic?
- let total_sep_len = sep_len * sep_count.checked_sub(1).unwrap_or(0);
+ let total_sep_len = sep.len() * sep_count.checked_sub(1).unwrap_or(0);
let real_total = total_width + total_sep_len;
if real_total <= limit && !pre_line_comments &&
// Now that we know how we will layout, we can decide for sure if there
// will be a trailing separator.
- let trailing_separator = needs_trailing_separator(formatting.trailing_separator, tactic);
- let mut result = String::new();
+ 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<usize> = None;
+ let mut sep_place = SeparatorPlace::from_tactic(formatting.separator_place, tactic);
let mut line_len = 0;
let indent_str = &formatting.shape.indent.to_string(formatting.config);
let inner_item = try_opt!(item.item.as_ref());
let first = i == 0;
let last = iter.peek().is_none();
- let separate = !last || trailing_separator;
+ let mut separate = !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)
result.push('\n');
result.push_str(indent_str);
line_len = 0;
+ if formatting.ends_with_newline {
+ if last {
+ separate = true;
+ } else {
+ trailing_separator = true;
+ }
+ }
+ sep_place = formatting.separator_place;
+ } else {
+ sep_place = SeparatorPlace::Back;
}
if line_len > 0 {
result.push_str(&comment);
if tactic == DefinitiveListTactic::Vertical {
- result.push('\n');
- result.push_str(indent_str);
+ // 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);
+ }
} 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
result.push_str(&formatted_comment);
}
- if separate {
+ if separate && sep_place.is_back() {
result.push_str(formatting.separator);
}
formatting.config.max_width(),
));
}
- let overhead = if let &mut Some(max_width) = item_max_width {
+ let overhead = if let Some(max_width) = *item_max_width {
max_width + 2
} else {
// 1 = space between item and comment.
for _ in 0..(comment_alignment + 1) {
result.push(' ');
}
- // An additional space for the missing trailing comma
- if last && item_max_width.is_some() && !separate {
+ // An additional space for the missing trailing separator.
+ if last && item_max_width.is_some() && !separate && !formatting.separator.is_empty()
+ {
result.push(' ');
}
}
item_max_width = None;
}
- if !last && tactic == DefinitiveListTactic::Vertical && item.new_lines {
+ if formatting.preserve_newline && !last && tactic == DefinitiveListTactic::Vertical &&
+ item.new_lines
+ {
item_max_width = None;
result.push('\n');
}
let item = item.as_ref();
let inner_item_width = item.inner_as_ref().len();
if !first &&
- (item.is_multiline() || !item.post_comment.is_some() ||
- inner_item_width + overhead > max_budget)
+ (item.is_different_group() || !item.post_comment.is_some() ||
+ inner_item_width + overhead > max_budget)
{
return max_width;
}
prev_span_end: BytePos,
next_span_start: BytePos,
terminator: &'a str,
+ leave_last: bool,
}
impl<'a, T, I, F1, F2, F3> Iterator for ListItems<'a, I, F1, F2, F3>
.span_to_snippet(mk_sp(self.prev_span_end, (self.get_lo)(&item)))
.unwrap();
let trimmed_pre_snippet = pre_snippet.trim();
- let has_pre_comment =
- trimmed_pre_snippet.contains("//") || trimmed_pre_snippet.contains("/*");
- let pre_comment = if has_pre_comment {
- Some(trimmed_pre_snippet.to_owned())
+ let has_single_line_comment = trimmed_pre_snippet.starts_with("//");
+ let has_block_comment = trimmed_pre_snippet.starts_with("/*");
+ let (pre_comment, pre_comment_style) = if has_single_line_comment {
+ (
+ Some(trimmed_pre_snippet.to_owned()),
+ ListItemCommentStyle::DifferentLine,
+ )
+ } else if has_block_comment {
+ let comment_end = pre_snippet.chars().rev().position(|c| c == '/').unwrap();
+ if pre_snippet
+ .chars()
+ .rev()
+ .take(comment_end + 1)
+ .any(|c| c == '\n')
+ {
+ (
+ Some(trimmed_pre_snippet.to_owned()),
+ ListItemCommentStyle::DifferentLine,
+ )
+ } else {
+ (
+ Some(trimmed_pre_snippet.to_owned()),
+ ListItemCommentStyle::SameLine,
+ )
+ }
} else {
- None
+ (None, ListItemCommentStyle::None)
};
// Post-comment
}
}
let newline_index = post_snippet.find('\n');
- let separator_index = post_snippet.find_uncommented(",").unwrap();
-
- 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(),
+ if let Some(separator_index) = post_snippet.find_uncommented(",") {
+ 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 {
+ // 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.
+ 1
}
}
None => post_snippet
.find_uncommented(self.terminator)
- .unwrap_or(post_snippet.len()),
+ .unwrap_or_else(|| post_snippet.len()),
};
if !post_snippet.is_empty() && comment_end > 0 {
// Everything from the separator to the next item.
let test_snippet = &post_snippet[comment_end - 1..];
- let first_newline = test_snippet.find('\n').unwrap_or(test_snippet.len());
+ 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(test_snippet.len());
+ .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];
self.prev_span_end = (self.get_hi)(&item) + BytePos(comment_end as u32);
let post_snippet = post_snippet[..comment_end].trim();
- let post_snippet_trimmed = if post_snippet.starts_with(',') {
+ let post_snippet_trimmed = if post_snippet.starts_with(|c| c == ',' || c == ':') {
post_snippet[1..].trim_matches(white_space)
} else if post_snippet.ends_with(',') {
post_snippet[..(post_snippet.len() - 1)].trim_matches(white_space)
ListItem {
pre_comment: pre_comment,
- item: (self.get_item_string)(&item),
+ pre_comment_style: pre_comment_style,
+ item: if self.inner.peek().is_none() && self.leave_last {
+ None
+ } else {
+ (self.get_item_string)(&item)
+ },
post_comment: post_comment,
new_lines: new_lines,
}
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>,
prev_span_end: prev_span_end,
next_span_start: next_span_start,
terminator: terminator,
- }
-}
-
-fn needs_trailing_separator(
- separator_tactic: SeparatorTactic,
- list_tactic: DefinitiveListTactic,
-) -> bool {
- match separator_tactic {
- SeparatorTactic::Always => true,
- SeparatorTactic::Vertical => list_tactic == DefinitiveListTactic::Vertical,
- SeparatorTactic::Never => false,
+ leave_last: leave_last,
}
}
IndentStyle::Block => {
let shape = shape.block_indent(context.config.tab_spaces());
Shape {
- width: try_opt!(context.config.max_width().checked_sub(shape.indent.width())),
+ width: context.budget(shape.indent.width()),
..shape
}
}
(IndentStyle::Visual, 1) => ListTactic::HorizontalVertical,
_ => context.config.struct_lit_multiline_style().to_list_tactic(),
};
- definitive_tactic(items, prelim_tactic, h_shape.width)
+ definitive_tactic(items, prelim_tactic, Separator::Comma, h_shape.width)
} else {
DefinitiveListTactic::Vertical
}
} else {
context.config.trailing_comma()
},
+ separator_place: SeparatorPlace::Back,
shape: shape,
ends_with_newline: ends_with_newline,
+ preserve_newline: true,
config: context.config,
}
}