1 // Copyright 2015 The Rust Project Developers. See the COPYRIGHT
2 // file at the top-level directory of this distribution and at
3 // http://rust-lang.org/COPYRIGHT.
5 // Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
6 // http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
7 // <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
8 // option. This file may not be copied, modified, or distributed
9 // except according to those terms.
13 use syntax::codemap::{self, CodeMap, BytePos};
15 use utils::{round_up_to_power_of_two, make_indent};
16 use comment::{FindUncommented, rewrite_comment, find_comment_end};
19 #[derive(Eq, PartialEq, Debug, Copy, Clone)]
23 // All items on one row.
25 // Try Horizontal layout, if that fails then vertical
27 // Pack as many items as possible per row over (possibly) many rows.
31 #[derive(Eq, PartialEq, Debug, Copy, Clone)]
32 pub enum SeparatorTactic {
38 impl_enum_decodable!(SeparatorTactic, Always, Never, Vertical);
40 // TODO having some helpful ctors for ListFormatting would be nice.
41 pub struct ListFormatting<'a> {
42 pub tactic: ListTactic,
43 pub separator: &'a str,
44 pub trailing_separator: SeparatorTactic,
46 // Available width if we layout horizontally.
48 // Available width if we layout vertically
50 // Non-expressions, e.g. items, will have a new line at the end of the list.
51 // Important for comment styles.
52 pub is_expression: bool
56 pub pre_comment: Option<String>,
57 // Item should include attributes and doc comments
59 pub post_comment: Option<String>
63 pub fn is_multiline(&self) -> bool {
64 self.item.contains('\n') ||
65 self.pre_comment.is_some() ||
66 self.post_comment.as_ref().map(|s| s.contains('\n')).unwrap_or(false)
69 pub fn from_str<S: Into<String>>(s: S) -> ListItem {
78 // Format a list of commented items into a string.
79 // FIXME: this has grown into a monstrosity
80 // TODO: add unit tests
81 pub fn write_list<'b>(items: &[ListItem], formatting: &ListFormatting<'b>) -> String {
86 let mut tactic = formatting.tactic;
88 // Conservatively overestimates because of the changing separator tactic.
89 let sep_count = if formatting.trailing_separator != SeparatorTactic::Never {
94 let sep_len = formatting.separator.len();
95 let total_sep_len = (sep_len + 1) * sep_count;
96 let total_width = calculate_width(items);
97 let fits_single = total_width + total_sep_len <= formatting.h_width;
99 // Check if we need to fallback from horizontal listing, if possible.
100 if tactic == ListTactic::HorizontalVertical {
101 debug!("write_list: total_width: {}, total_sep_len: {}, h_width: {}",
102 total_width, total_sep_len, formatting.h_width);
103 tactic = if fits_single &&
104 !items.iter().any(ListItem::is_multiline) {
105 ListTactic::Horizontal
111 // Check if we can fit everything on a single line in mixed mode.
112 // The horizontal tactic does not break after v_width columns.
113 if tactic == ListTactic::Mixed && fits_single {
114 tactic = ListTactic::Horizontal;
117 // Switch to vertical mode if we find non-block comments.
118 if items.iter().any(has_line_pre_comment) {
119 tactic = ListTactic::Vertical;
122 // Now that we know how we will layout, we can decide for sure if there
123 // will be a trailing separator.
124 let trailing_separator = needs_trailing_separator(formatting.trailing_separator, tactic);
126 // Create a buffer for the result.
127 // TODO could use a StringBuffer or rope for this
128 let alloc_width = if tactic == ListTactic::Horizontal {
129 total_width + total_sep_len
131 total_width + items.len() * (formatting.indent + 1)
133 let mut result = String::with_capacity(round_up_to_power_of_two(alloc_width));
135 let mut line_len = 0;
136 let indent_str = &make_indent(formatting.indent);
137 for (i, item) in items.iter().enumerate() {
139 let last = i == items.len() - 1;
140 let separate = !last || trailing_separator;
141 let item_sep_len = if separate { sep_len } else { 0 };
142 let item_width = item.item.len() + item_sep_len;
145 ListTactic::Horizontal if !first => {
148 ListTactic::Vertical if !first => {
150 result.push_str(indent_str);
152 ListTactic::Mixed => {
153 let total_width = total_item_width(item) + item_sep_len;
155 if line_len > 0 && line_len + total_width > formatting.v_width {
157 result.push_str(indent_str);
166 line_len += total_width;
172 if let Some(ref comment) = item.pre_comment {
173 result.push_str(&rewrite_comment(comment,
174 // Block style in non-vertical mode
175 tactic != ListTactic::Vertical,
179 if tactic == ListTactic::Vertical {
181 result.push_str(indent_str);
187 result.push_str(&item.item);
190 if tactic != ListTactic::Vertical && item.post_comment.is_some() {
191 // We'll assume it'll fit on one line at this point
192 let formatted_comment = rewrite_comment(item.post_comment.as_ref().unwrap(),
198 result.push_str(&formatted_comment);
202 result.push_str(formatting.separator);
205 if tactic == ListTactic::Vertical && item.post_comment.is_some() {
206 // 1 = space between item and comment.
207 let width = formatting.v_width.checked_sub(item_width + 1).unwrap_or(1);
208 let offset = formatting.indent + item_width + 1;
209 let comment = item.post_comment.as_ref().unwrap();
210 // Use block-style only for the last item or multiline comments.
211 let block_style = formatting.is_expression && last ||
212 comment.trim().contains('\n') ||
213 comment.trim().len() > width;
215 let formatted_comment = rewrite_comment(comment,
221 result.push_str(&formatted_comment);
228 fn has_line_pre_comment(item: &ListItem) -> bool {
229 match item.pre_comment {
230 Some(ref comment) => comment.starts_with("//"),
235 // Turns a list into a vector of items with associated comments.
236 // TODO: we probably do not want to take a terminator any more. Instead, we
237 // should demand a proper span end.
238 pub fn itemize_list<T, I, F1, F2, F3>(codemap: &CodeMap,
239 prefix: Vec<ListItem>,
246 mut prev_span_end: BytePos,
247 next_span_start: BytePos)
249 where I: Iterator<Item=T>,
250 F1: Fn(&T) -> BytePos,
251 F2: Fn(&T) -> BytePos,
254 let mut result = prefix;
255 let mut new_it = it.peekable();
256 let white_space: &[_] = &[' ', '\t'];
258 while let Some(item) = new_it.next() {
260 let pre_snippet = codemap.span_to_snippet(codemap::mk_sp(prev_span_end,
263 let pre_snippet = pre_snippet.trim();
264 let pre_comment = if pre_snippet.len() > 0 {
265 Some(pre_snippet.to_owned())
271 let next_start = match new_it.peek() {
272 Some(ref next_item) => get_lo(next_item),
273 None => next_span_start
275 let post_snippet = codemap.span_to_snippet(codemap::mk_sp(get_hi(&item),
279 let comment_end = match new_it.peek() {
281 if let Some(start) = before(&post_snippet, "/*", "\n") {
282 // Block-style post-comment. Either before or after the separator.
283 cmp::max(find_comment_end(&post_snippet[start..]).unwrap() + start,
284 post_snippet.find_uncommented(separator).unwrap() + separator.len())
285 } else if let Some(idx) = post_snippet.find('\n') {
292 post_snippet.find_uncommented(terminator)
293 .unwrap_or(post_snippet.len())
297 prev_span_end = get_hi(&item) + BytePos(comment_end as u32);
298 let mut post_snippet = post_snippet[..comment_end].trim();
300 if post_snippet.starts_with(separator) {
301 post_snippet = post_snippet[separator.len()..]
302 .trim_matches(white_space);
303 } else if post_snippet.ends_with(separator) {
304 post_snippet = post_snippet[..post_snippet.len()-separator.len()]
305 .trim_matches(white_space);
308 result.push(ListItem {
309 pre_comment: pre_comment,
310 item: get_item_string(&item),
311 post_comment: if post_snippet.len() > 0 {
312 Some(post_snippet.to_owned())
322 fn needs_trailing_separator(separator_tactic: SeparatorTactic, list_tactic: ListTactic) -> bool {
323 match separator_tactic {
324 SeparatorTactic::Always => true,
325 SeparatorTactic::Vertical => list_tactic == ListTactic::Vertical,
326 SeparatorTactic::Never => false,
330 fn calculate_width(items: &[ListItem]) -> usize {
331 items.iter().map(total_item_width).fold(0, |a, l| a + l)
334 fn total_item_width(item: &ListItem) -> usize {
335 comment_len(&item.pre_comment) + comment_len(&item.post_comment) + item.item.len()
338 fn comment_len(comment: &Option<String>) -> usize {
341 let text_len = s.trim().len();
343 // We'll put " /*" before and " */" after inline comments.