]> git.lizzy.rs Git - rust.git/blob - src/lists.rs
Run rustfmt on the code
[rust.git] / src / lists.rs
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.
4 //
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.
10
11 use std::cmp;
12
13 use syntax::codemap::{self, CodeMap, BytePos};
14
15 use utils::{round_up_to_power_of_two, make_indent};
16 use comment::{FindUncommented, rewrite_comment, find_comment_end};
17
18 #[derive(Eq, PartialEq, Debug, Copy, Clone)]
19 pub enum ListTactic {
20     // One item per row.
21     Vertical,
22     // All items on one row.
23     Horizontal,
24     // Try Horizontal layout, if that fails then vertical
25     HorizontalVertical,
26     // Pack as many items as possible per row over (possibly) many rows.
27     Mixed,
28 }
29
30 #[derive(Eq, PartialEq, Debug, Copy, Clone)]
31 pub enum SeparatorTactic {
32     Always,
33     Never,
34     Vertical,
35 }
36
37 impl_enum_decodable!(SeparatorTactic, Always, Never, Vertical);
38
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,
44     pub indent: usize,
45     // Available width if we layout horizontally.
46     pub h_width: usize,
47     // Available width if we layout vertically
48     pub v_width: usize,
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,
52 }
53
54 pub struct ListItem {
55     pub pre_comment: Option<String>,
56     // Item should include attributes and doc comments
57     pub item: String,
58     pub post_comment: Option<String>,
59 }
60
61 impl ListItem {
62     pub fn is_multiline(&self) -> bool {
63         self.item.contains('\n') || self.pre_comment.is_some() ||
64         self.post_comment.as_ref().map(|s| s.contains('\n')).unwrap_or(false)
65     }
66
67     pub fn has_line_pre_comment(&self) -> bool {
68         self.pre_comment.as_ref().map_or(false, |comment| comment.starts_with("//"))
69     }
70
71     pub fn from_str<S: Into<String>>(s: S) -> ListItem {
72         ListItem { pre_comment: None, item: s.into(), post_comment: None }
73     }
74 }
75
76 // Format a list of commented items into a string.
77 // FIXME: this has grown into a monstrosity
78 // TODO: add unit tests
79 pub fn write_list<'b>(items: &[ListItem], formatting: &ListFormatting<'b>) -> String {
80     if items.len() == 0 {
81         return String::new();
82     }
83
84     let mut tactic = formatting.tactic;
85
86     // Conservatively overestimates because of the changing separator tactic.
87     let sep_count = if formatting.trailing_separator != SeparatorTactic::Never {
88         items.len()
89     } else {
90         items.len() - 1
91     };
92     let sep_len = formatting.separator.len();
93     let total_sep_len = (sep_len + 1) * sep_count;
94     let total_width = calculate_width(items);
95     let fits_single = total_width + total_sep_len <= formatting.h_width;
96
97     // Check if we need to fallback from horizontal listing, if possible.
98     if tactic == ListTactic::HorizontalVertical {
99         debug!("write_list: total_width: {}, total_sep_len: {}, h_width: {}",
100                total_width, total_sep_len, formatting.h_width);
101         tactic = if fits_single &&
102                     !items.iter().any(ListItem::is_multiline) {
103             ListTactic::Horizontal
104         } else {
105             ListTactic::Vertical
106         };
107     }
108
109     // Check if we can fit everything on a single line in mixed mode.
110     // The horizontal tactic does not break after v_width columns.
111     if tactic == ListTactic::Mixed && fits_single {
112         tactic = ListTactic::Horizontal;
113     }
114
115     // Switch to vertical mode if we find non-block comments.
116     if items.iter().any(ListItem::has_line_pre_comment) {
117         tactic = ListTactic::Vertical;
118     }
119
120     // Now that we know how we will layout, we can decide for sure if there
121     // will be a trailing separator.
122     let trailing_separator = needs_trailing_separator(formatting.trailing_separator, tactic);
123
124     // Create a buffer for the result.
125     // TODO could use a StringBuffer or rope for this
126     let alloc_width = if tactic == ListTactic::Horizontal {
127         total_width + total_sep_len
128     } else {
129         total_width + items.len() * (formatting.indent + 1)
130     };
131     let mut result = String::with_capacity(round_up_to_power_of_two(alloc_width));
132
133     let mut line_len = 0;
134     let indent_str = &make_indent(formatting.indent);
135     for (i, item) in items.iter().enumerate() {
136         let first = i == 0;
137         let last = i == items.len() - 1;
138         let separate = !last || trailing_separator;
139         let item_sep_len = if separate { sep_len } else { 0 };
140         let item_width = item.item.len() + item_sep_len;
141
142         match tactic {
143             ListTactic::Horizontal if !first => {
144                 result.push(' ');
145             }
146             ListTactic::Vertical if !first => {
147                 result.push('\n');
148                 result.push_str(indent_str);
149             }
150             ListTactic::Mixed => {
151                 let total_width = total_item_width(item) + item_sep_len;
152
153                 if line_len > 0 && line_len + total_width > formatting.v_width {
154                     result.push('\n');
155                     result.push_str(indent_str);
156                     line_len = 0;
157                 }
158
159                 if line_len > 0 {
160                     result.push(' ');
161                     line_len += 1;
162                 }
163
164                 line_len += total_width;
165             }
166             _ => {}
167         }
168
169         // Pre-comments
170         if let Some(ref comment) = item.pre_comment {
171             result.push_str(&rewrite_comment(comment,
172                                              // Block style in non-vertical mode
173                                              tactic != ListTactic::Vertical,
174                                              // Width restriction is only
175                                              // relevant in vertical mode.
176                                              formatting.v_width,
177                                              formatting.indent));
178
179             if tactic == ListTactic::Vertical {
180                 result.push('\n');
181                 result.push_str(indent_str);
182             } else {
183                 result.push(' ');
184             }
185         }
186
187         result.push_str(&item.item);
188
189         // Post-comments
190         if tactic != ListTactic::Vertical && item.post_comment.is_some() {
191             let formatted_comment = rewrite_comment(item.post_comment.as_ref().unwrap(),
192                                                     true,
193                                                     formatting.v_width,
194                                                     0);
195
196             result.push(' ');
197             result.push_str(&formatted_comment);
198         }
199
200         if separate {
201             result.push_str(formatting.separator);
202         }
203
204         if tactic == ListTactic::Vertical && item.post_comment.is_some() {
205             // 1 = space between item and comment.
206             let width = formatting.v_width.checked_sub(item_width + 1).unwrap_or(1);
207             let offset = formatting.indent + item_width + 1;
208             let comment = item.post_comment.as_ref().unwrap();
209             // Use block-style only for the last item or multiline comments.
210             let block_style = formatting.ends_with_newline && last ||
211                               comment.trim().contains('\n') ||
212                               comment.trim().len() > width;
213
214             let formatted_comment = rewrite_comment(comment, block_style, width, offset);
215
216             result.push(' ');
217             result.push_str(&formatted_comment);
218         }
219     }
220
221     result
222 }
223
224 // Turns a list into a vector of items with associated comments.
225 // TODO: we probably do not want to take a terminator any more. Instead, we
226 // should demand a proper span end.
227 pub fn itemize_list<T, I, F1, F2, F3>(codemap: &CodeMap,
228                                       prefix: Vec<ListItem>,
229                                       it: I,
230                                       separator: &str,
231                                       terminator: &str,
232                                       get_lo: F1,
233                                       get_hi: F2,
234                                       get_item_string: F3,
235                                       mut prev_span_end: BytePos,
236                                       next_span_start: BytePos)
237                                       -> Vec<ListItem>
238     where I: Iterator<Item = T>,
239           F1: Fn(&T) -> BytePos,
240           F2: Fn(&T) -> BytePos,
241           F3: Fn(&T) -> String
242 {
243     let mut result = prefix;
244     result.reserve(it.size_hint().0);
245
246     let mut new_it = it.peekable();
247     let white_space: &[_] = &[' ', '\t'];
248
249     while let Some(item) = new_it.next() {
250         // Pre-comment
251         let pre_snippet = codemap.span_to_snippet(codemap::mk_sp(prev_span_end,
252                                                                 get_lo(&item)))
253                                  .unwrap();
254         let pre_snippet = pre_snippet.trim();
255         let pre_comment = if pre_snippet.len() > 0 {
256             Some(pre_snippet.to_owned())
257         } else {
258             None
259         };
260
261         // Post-comment
262         let next_start = match new_it.peek() {
263             Some(ref next_item) => get_lo(next_item),
264             None => next_span_start
265         };
266         let post_snippet = codemap.span_to_snippet(codemap::mk_sp(get_hi(&item),
267                                                                   next_start))
268                                   .unwrap();
269
270         let comment_end = match new_it.peek() {
271             Some(..) => {
272                 let block_open_index = post_snippet.find("/*");
273                 let newline_index = post_snippet.find('\n');
274                 let separator_index = post_snippet.find_uncommented(separator).unwrap();
275
276                 match (block_open_index, newline_index) {
277                     // Separator before comment, with the next item on same line.
278                     // Comment belongs to next item.
279                     (Some(i), None) if i > separator_index => { separator_index + separator.len() }
280                     // Block-style post-comment before the separator.
281                     (Some(i), None) => {
282                         cmp::max(find_comment_end(&post_snippet[i..]).unwrap() + i,
283                                  separator_index + separator.len())
284                     }
285                     // Block-style post-comment. Either before or after the separator.
286                     (Some(i), Some(j)) if i < j => {
287                         cmp::max(find_comment_end(&post_snippet[i..]).unwrap() + i,
288                                  separator_index + separator.len())
289                     }
290                     // Potential *single* line comment.
291                     (_, Some(j)) => { j + 1 }
292                     _ => post_snippet.len()
293                 }
294             },
295             None => {
296                 post_snippet.find_uncommented(terminator)
297                             .unwrap_or(post_snippet.len())
298             }
299         };
300
301         // Cleanup post-comment: strip separators and whitespace.
302         prev_span_end = get_hi(&item) + BytePos(comment_end as u32);
303         let mut post_snippet = post_snippet[..comment_end].trim();
304
305         if post_snippet.starts_with(separator) {
306             post_snippet = post_snippet[separator.len()..]
307                 .trim_matches(white_space);
308         } else if post_snippet.ends_with(separator) {
309             post_snippet = post_snippet[..post_snippet.len()-separator.len()]
310                 .trim_matches(white_space);
311         }
312
313         result.push(ListItem {
314             pre_comment: pre_comment,
315             item: get_item_string(&item),
316             post_comment: if post_snippet.len() > 0 {
317                 Some(post_snippet.to_owned())
318             } else {
319                 None
320             }
321         });
322     }
323
324     result
325 }
326
327 fn needs_trailing_separator(separator_tactic: SeparatorTactic, list_tactic: ListTactic) -> bool {
328     match separator_tactic {
329         SeparatorTactic::Always => true,
330         SeparatorTactic::Vertical => list_tactic == ListTactic::Vertical,
331         SeparatorTactic::Never => false,
332     }
333 }
334
335 fn calculate_width(items: &[ListItem]) -> usize {
336     items.iter().map(total_item_width).fold(0, |a, l| a + l)
337 }
338
339 fn total_item_width(item: &ListItem) -> usize {
340     comment_len(&item.pre_comment) + comment_len(&item.post_comment) + item.item.len()
341 }
342
343 fn comment_len(comment: &Option<String>) -> usize {
344     match comment {
345         &Some(ref s) => {
346             let text_len = s.trim().len();
347             if text_len > 0 {
348                 // We'll put " /*" before and " */" after inline comments.
349                 text_len + 6
350             } else {
351                 text_len
352             }
353         },
354         &None => 0
355     }
356 }