use std::cmp;
use std::iter::Peekable;
-use syntax::codemap::{CodeMap, BytePos};
+use syntax::codemap::{BytePos, CodeMap};
use {Indent, Shape};
-use comment::{FindUncommented, rewrite_comment, find_comment_end};
+use comment::{find_comment_end, rewrite_comment, FindUncommented};
use config::{Config, IndentStyle};
use rewrite::RewriteContext;
-use utils::mk_sp;
+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,
}
-pub fn format_fn_args<I>(items: I, shape: Shape, config: &Config) -> Option<String>
-where
- I: Iterator<Item = ListItem>,
-{
- list_helper(
- items,
- shape,
- config,
- ListTactic::LimitedHorizontalVertical(config.fn_call_width()),
- )
-}
-
-pub fn format_item_list<I>(items: I, shape: Shape, config: &Config) -> Option<String>
-where
- I: Iterator<Item = ListItem>,
-{
- list_helper(items, shape, config, ListTactic::HorizontalVertical)
-}
-
-pub fn list_helper<I>(items: I, shape: Shape, config: &Config, tactic: ListTactic) -> Option<String>
-where
- I: Iterator<Item = ListItem>,
-{
- let item_vec: Vec<_> = items.collect();
- let tactic = definitive_tactic(&item_vec, tactic, shape.width);
- let fmt = ListFormatting {
- tactic: tactic,
- separator: ",",
- trailing_separator: SeparatorTactic::Never,
- shape: shape,
- ends_with_newline: false,
- config: config,
- };
-
- write_list(&item_vec, &fmt)
+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 {
}
}
+#[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)
+ }
+
+ 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.item.as_ref().map_or(false, |s| s.contains('\n')) || self.pre_comment.is_some() ||
+ 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_line_pre_comment(&self) -> bool {
+ pub fn has_comment(&self) -> bool {
self.pre_comment
.as_ref()
- .map_or(false, |comment| comment.starts_with("//"))
+ .map_or(false, |comment| comment.starts_with("//")) ||
+ self.post_comment
+ .as_ref()
+ .map_or(false, |comment| comment.starts_with("//"))
}
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 pre_line_comments = items
.clone()
.into_iter()
- .any(|item| item.as_ref().has_line_pre_comment());
+ .any(|item| item.as_ref().has_comment());
let limit = match tactic {
_ if pre_line_comments => return DefinitiveListTactic::Vertical,
};
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 &&
// TODO: add unit tests
pub fn write_list<I, T>(items: I, formatting: &ListFormatting) -> Option<String>
where
- I: IntoIterator<Item = T>,
+ I: IntoIterator<Item = T> + Clone,
T: AsRef<ListItem>,
{
let tactic = formatting.tactic;
// 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);
}
if tactic == DefinitiveListTactic::Vertical && item.post_comment.is_some() {
- // 1 = space between item and comment.
- let width = formatting
- .shape
- .width
- .checked_sub(item_last_line_width + 1)
- .unwrap_or(1);
- let mut offset = formatting.shape.indent;
- offset.alignment += item_last_line_width + 1;
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<usize>| {
+ 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 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, block_style, comment_shape, formatting.config)
+ };
- debug!("Width = {}, offset = {:?}", 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;
-
- let formatted_comment = try_opt!(rewrite_comment(
- comment,
- block_style,
- Shape::legacy(width, offset),
- formatting.config,
- ));
+ let mut formatted_comment = try_opt!(rewrite_post_comment(&mut item_max_width));
if !formatted_comment.starts_with('\n') {
- result.push(' ');
+ 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 = try_opt!(rewrite_post_comment(&mut item_max_width));
+ comment_alignment = post_comment_alignment(item_max_width, inner_item.len());
+ }
+ for _ in 0..(comment_alignment + 1) {
+ result.push(' ');
+ }
+ // An additional space for the missing trailing separator.
+ if last && item_max_width.is_some() && !separate && !formatting.separator.is_empty()
+ {
+ result.push(' ');
+ }
+ }
+ if formatted_comment.contains('\n') {
+ item_max_width = None;
}
result.push_str(&formatted_comment);
+ } else {
+ 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');
}
}
Some(result)
}
+fn max_width_of_item_with_post_comment<I, T>(
+ items: &I,
+ i: usize,
+ overhead: usize,
+ max_budget: usize,
+) -> usize
+where
+ I: IntoIterator<Item = T> + Clone,
+ T: AsRef<ListItem>,
+{
+ 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_some() ||
+ 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<usize>, inner_item_len: usize) -> usize {
+ item_max_width
+ .and_then(|max_line_width| max_line_width.checked_sub(inner_item_len))
+ .unwrap_or(0)
+}
+
pub struct ListItems<'a, I, F1, F2, F3>
where
I: Iterator,
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(
+ 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(
+ ),
+ // 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(),
}
- // 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())
- }
+ None => post_snippet
+ .find_uncommented(self.terminator)
+ .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,
}
}
suffix_width: usize,
) -> Option<(Option<Shape>, Shape)> {
let v_shape = match context.config.struct_lit_style() {
- IndentStyle::Visual => {
- try_opt!(
- try_opt!(shape.visual_indent(0).shrink_left(prefix_width)).sub_width(suffix_width)
- )
- }
+ IndentStyle::Visual => try_opt!(
+ try_opt!(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: try_opt!(context.config.max_width().checked_sub(shape.indent.width())),
+ width: context.budget(shape.indent.width()),
..shape
}
}
};
- let h_shape = shape.sub_width(prefix_width + suffix_width);
- Some((h_shape, v_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.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.
items: &[ListItem],
) -> DefinitiveListTactic {
if let Some(h_shape) = h_shape {
- let mut prelim_tactic = match (context.config.struct_lit_style(), items.len()) {
+ let prelim_tactic = match (context.config.struct_lit_style(), items.len()) {
(IndentStyle::Visual, 1) => ListTactic::HorizontalVertical,
_ => context.config.struct_lit_multiline_style().to_list_tactic(),
};
-
- if prelim_tactic == ListTactic::HorizontalVertical && items.len() > 1 {
- prelim_tactic =
- ListTactic::LimitedHorizontalVertical(context.config.struct_lit_width());
- }
-
- 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,
}
}