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};
18 #[derive(Eq, PartialEq, Debug, Copy, Clone)]
22 // All items on one row.
24 // Try Horizontal layout, if that fails then vertical
26 // Pack as many items as possible per row over (possibly) many rows.
30 #[derive(Eq, PartialEq, Debug, Copy, Clone)]
31 pub enum SeparatorTactic {
37 impl_enum_decodable!(SeparatorTactic, Always, Never, Vertical);
39 // TODO having some helpful ctors for ListFormatting would be nice.
40 pub struct ListFormatting<'a> {
41 pub tactic: ListTactic,
42 pub separator: &'a str,
43 pub trailing_separator: SeparatorTactic,
45 // Available width if we layout horizontally.
47 // Available width if we layout vertically
49 // Non-expressions, e.g. items, will have a new line at the end of the list.
50 // Important for comment styles.
51 pub ends_with_newline: bool
55 pub pre_comment: Option<String>,
56 // Item should include attributes and doc comments
58 pub post_comment: Option<String>
62 pub fn is_multiline(&self) -> bool {
63 self.item.contains('\n') ||
64 self.pre_comment.is_some() ||
65 self.post_comment.as_ref().map(|s| s.contains('\n')).unwrap_or(false)
68 pub fn has_line_pre_comment(&self) -> bool {
69 self.pre_comment.as_ref().map_or(false, |comment| comment.starts_with("//"))
72 pub fn from_str<S: Into<String>>(s: S) -> ListItem {
81 // Format a list of commented items into a string.
82 // FIXME: this has grown into a monstrosity
83 // TODO: add unit tests
84 pub fn write_list<'b>(items: &[ListItem], formatting: &ListFormatting<'b>) -> String {
89 let mut tactic = formatting.tactic;
91 // Conservatively overestimates because of the changing separator tactic.
92 let sep_count = if formatting.trailing_separator != SeparatorTactic::Never {
97 let sep_len = formatting.separator.len();
98 let total_sep_len = (sep_len + 1) * sep_count;
99 let total_width = calculate_width(items);
100 let fits_single = total_width + total_sep_len <= formatting.h_width;
102 // Check if we need to fallback from horizontal listing, if possible.
103 if tactic == ListTactic::HorizontalVertical {
104 debug!("write_list: total_width: {}, total_sep_len: {}, h_width: {}",
105 total_width, total_sep_len, formatting.h_width);
106 tactic = if fits_single &&
107 !items.iter().any(ListItem::is_multiline) {
108 ListTactic::Horizontal
114 // Check if we can fit everything on a single line in mixed mode.
115 // The horizontal tactic does not break after v_width columns.
116 if tactic == ListTactic::Mixed && fits_single {
117 tactic = ListTactic::Horizontal;
120 // Switch to vertical mode if we find non-block comments.
121 if items.iter().any(ListItem::has_line_pre_comment) {
122 tactic = ListTactic::Vertical;
125 // Now that we know how we will layout, we can decide for sure if there
126 // will be a trailing separator.
127 let trailing_separator = needs_trailing_separator(formatting.trailing_separator, tactic);
129 // Create a buffer for the result.
130 // TODO could use a StringBuffer or rope for this
131 let alloc_width = if tactic == ListTactic::Horizontal {
132 total_width + total_sep_len
134 total_width + items.len() * (formatting.indent + 1)
136 let mut result = String::with_capacity(round_up_to_power_of_two(alloc_width));
138 let mut line_len = 0;
139 let indent_str = &make_indent(formatting.indent);
140 for (i, item) in items.iter().enumerate() {
142 let last = i == items.len() - 1;
143 let separate = !last || trailing_separator;
144 let item_sep_len = if separate { sep_len } else { 0 };
145 let item_width = item.item.len() + item_sep_len;
148 ListTactic::Horizontal if !first => {
151 ListTactic::Vertical if !first => {
153 result.push_str(indent_str);
155 ListTactic::Mixed => {
156 let total_width = total_item_width(item) + item_sep_len;
158 if line_len > 0 && line_len + total_width > formatting.v_width {
160 result.push_str(indent_str);
169 line_len += total_width;
175 if let Some(ref comment) = item.pre_comment {
176 result.push_str(&rewrite_comment(comment,
177 // Block style in non-vertical mode
178 tactic != ListTactic::Vertical,
179 // Width restriction is only
180 // relevant in vertical mode.
184 if tactic == ListTactic::Vertical {
186 result.push_str(indent_str);
192 result.push_str(&item.item);
195 if tactic != ListTactic::Vertical && item.post_comment.is_some() {
196 let formatted_comment = rewrite_comment(item.post_comment.as_ref().unwrap(),
202 result.push_str(&formatted_comment);
206 result.push_str(formatting.separator);
209 if tactic == ListTactic::Vertical && item.post_comment.is_some() {
210 // 1 = space between item and comment.
211 let width = formatting.v_width.checked_sub(item_width + 1).unwrap_or(1);
212 let offset = formatting.indent + item_width + 1;
213 let comment = item.post_comment.as_ref().unwrap();
214 // Use block-style only for the last item or multiline comments.
215 let block_style = formatting.ends_with_newline && last ||
216 comment.trim().contains('\n') ||
217 comment.trim().len() > width;
219 let formatted_comment = rewrite_comment(comment, block_style, width, offset);
222 result.push_str(&formatted_comment);
229 // Turns a list into a vector of items with associated comments.
230 // TODO: we probably do not want to take a terminator any more. Instead, we
231 // should demand a proper span end.
232 pub fn itemize_list<T, I, F1, F2, F3>(codemap: &CodeMap,
233 prefix: Vec<ListItem>,
240 mut prev_span_end: BytePos,
241 next_span_start: BytePos)
243 where I: Iterator<Item=T>,
244 F1: Fn(&T) -> BytePos,
245 F2: Fn(&T) -> BytePos,
248 let mut result = prefix;
249 result.reserve(it.size_hint().0);
251 let mut new_it = it.peekable();
252 let white_space: &[_] = &[' ', '\t'];
254 while let Some(item) = new_it.next() {
256 let pre_snippet = codemap.span_to_snippet(codemap::mk_sp(prev_span_end,
259 let pre_snippet = pre_snippet.trim();
260 let pre_comment = if pre_snippet.len() > 0 {
261 Some(pre_snippet.to_owned())
267 let next_start = match new_it.peek() {
268 Some(ref next_item) => get_lo(next_item),
269 None => next_span_start
271 let post_snippet = codemap.span_to_snippet(codemap::mk_sp(get_hi(&item),
275 let comment_end = match new_it.peek() {
277 let block_open_index = post_snippet.find("/*");
278 let newline_index = post_snippet.find('\n');
279 let separator_index = post_snippet.find_uncommented(separator).unwrap();
281 match (block_open_index, newline_index) {
282 // Separator before comment, with the next item on same line.
283 // Comment belongs to next item.
284 (Some(i), None) if i > separator_index => { separator_index + separator.len() }
285 // Block-style post-comment before the separator.
287 cmp::max(find_comment_end(&post_snippet[i..]).unwrap() + i,
288 separator_index + separator.len())
290 // Block-style post-comment. Either before or after the separator.
291 (Some(i), Some(j)) if i < j => {
292 cmp::max(find_comment_end(&post_snippet[i..]).unwrap() + i,
293 separator_index + separator.len())
295 // Potential *single* line comment.
296 (_, Some(j)) => { j + 1 }
297 _ => post_snippet.len()
301 post_snippet.find_uncommented(terminator)
302 .unwrap_or(post_snippet.len())
306 // Cleanup post-comment: strip separators and whitespace.
307 prev_span_end = get_hi(&item) + BytePos(comment_end as u32);
308 let mut post_snippet = post_snippet[..comment_end].trim();
310 if post_snippet.starts_with(separator) {
311 post_snippet = post_snippet[separator.len()..]
312 .trim_matches(white_space);
313 } else if post_snippet.ends_with(separator) {
314 post_snippet = post_snippet[..post_snippet.len()-separator.len()]
315 .trim_matches(white_space);
318 result.push(ListItem {
319 pre_comment: pre_comment,
320 item: get_item_string(&item),
321 post_comment: if post_snippet.len() > 0 {
322 Some(post_snippet.to_owned())
332 fn needs_trailing_separator(separator_tactic: SeparatorTactic, list_tactic: ListTactic) -> bool {
333 match separator_tactic {
334 SeparatorTactic::Always => true,
335 SeparatorTactic::Vertical => list_tactic == ListTactic::Vertical,
336 SeparatorTactic::Never => false,
340 fn calculate_width(items: &[ListItem]) -> usize {
341 items.iter().map(total_item_width).fold(0, |a, l| a + l)
344 fn total_item_width(item: &ListItem) -> usize {
345 comment_len(&item.pre_comment) + comment_len(&item.post_comment) + item.item.len()
348 fn comment_len(comment: &Option<String>) -> usize {
351 let text_len = s.trim().len();
353 // We'll put " /*" before and " */" after inline comments.