]> git.lizzy.rs Git - rust.git/blob - src/lists.rs
Merge pull request #113 from marcusklaas/import-comments
[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') ||
64         self.pre_comment.is_some() ||
65         self.post_comment.as_ref().map(|s| s.contains('\n')).unwrap_or(false)
66     }
67
68     pub fn has_line_pre_comment(&self) -> bool {
69         self.pre_comment.as_ref().map_or(false, |comment| comment.starts_with("//"))
70     }
71
72     pub fn from_str<S: Into<String>>(s: S) -> ListItem {
73         ListItem {
74             pre_comment: None,
75             item: s.into(),
76             post_comment: None
77         }
78     }
79 }
80
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 {
85     if items.len() == 0 {
86         return String::new();
87     }
88
89     let mut tactic = formatting.tactic;
90
91     // Conservatively overestimates because of the changing separator tactic.
92     let sep_count = if formatting.trailing_separator != SeparatorTactic::Never {
93         items.len()
94     } else {
95         items.len() - 1
96     };
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;
101
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
109         } else {
110             ListTactic::Vertical
111         };
112     }
113
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;
118     }
119
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;
123     }
124
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);
128
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
133     } else {
134         total_width + items.len() * (formatting.indent + 1)
135     };
136     let mut result = String::with_capacity(round_up_to_power_of_two(alloc_width));
137
138     let mut line_len = 0;
139     let indent_str = &make_indent(formatting.indent);
140     for (i, item) in items.iter().enumerate() {
141         let first = i == 0;
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;
146
147         match tactic {
148             ListTactic::Horizontal if !first => {
149                 result.push(' ');
150             }
151             ListTactic::Vertical if !first => {
152                 result.push('\n');
153                 result.push_str(indent_str);
154             }
155             ListTactic::Mixed => {
156                 let total_width = total_item_width(item) + item_sep_len;
157
158                 if line_len > 0 && line_len + total_width > formatting.v_width {
159                     result.push('\n');
160                     result.push_str(indent_str);
161                     line_len = 0;
162                 }
163
164                 if line_len > 0 {
165                     result.push(' ');
166                     line_len += 1;
167                 }
168
169                 line_len += total_width;
170             }
171             _ => {}
172         }
173
174         // Pre-comments
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.
181                                              formatting.v_width,
182                                              formatting.indent));
183
184             if tactic == ListTactic::Vertical {
185                 result.push('\n');
186                 result.push_str(indent_str);
187             } else {
188                 result.push(' ');
189             }
190         }
191
192         result.push_str(&item.item);
193
194         // Post-comments
195         if tactic != ListTactic::Vertical && item.post_comment.is_some() {
196             let formatted_comment = rewrite_comment(item.post_comment.as_ref().unwrap(),
197                                                     true,
198                                                     formatting.v_width,
199                                                     0);
200
201             result.push(' ');
202             result.push_str(&formatted_comment);
203         }
204
205         if separate {
206             result.push_str(formatting.separator);
207         }
208
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;
218
219             let formatted_comment = rewrite_comment(comment, block_style, width, offset);
220
221             result.push(' ');
222             result.push_str(&formatted_comment);
223         }
224     }
225
226     result
227 }
228
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>,
234                                       it: I,
235                                       separator: &str,
236                                       terminator: &str,
237                                       get_lo: F1,
238                                       get_hi: F2,
239                                       get_item_string: F3,
240                                       mut prev_span_end: BytePos,
241                                       next_span_start: BytePos)
242     -> Vec<ListItem>
243     where I: Iterator<Item=T>,
244           F1: Fn(&T) -> BytePos,
245           F2: Fn(&T) -> BytePos,
246           F3: Fn(&T) -> String
247 {
248     let mut result = prefix;
249     result.reserve(it.size_hint().0);
250
251     let mut new_it = it.peekable();
252     let white_space: &[_] = &[' ', '\t'];
253
254     while let Some(item) = new_it.next() {
255         // Pre-comment
256         let pre_snippet = codemap.span_to_snippet(codemap::mk_sp(prev_span_end,
257                                                                 get_lo(&item)))
258                                  .unwrap();
259         let pre_snippet = pre_snippet.trim();
260         let pre_comment = if pre_snippet.len() > 0 {
261             Some(pre_snippet.to_owned())
262         } else {
263             None
264         };
265
266         // Post-comment
267         let next_start = match new_it.peek() {
268             Some(ref next_item) => get_lo(next_item),
269             None => next_span_start
270         };
271         let post_snippet = codemap.span_to_snippet(codemap::mk_sp(get_hi(&item),
272                                                                   next_start))
273                                   .unwrap();
274
275         let comment_end = match new_it.peek() {
276             Some(..) => {
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();
280
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.
286                     (Some(i), None) => {
287                         cmp::max(find_comment_end(&post_snippet[i..]).unwrap() + i,
288                                  separator_index + separator.len())
289                     }
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())
294                     }
295                     // Potential *single* line comment.
296                     (_, Some(j)) => { j + 1 }
297                     _ => post_snippet.len()
298                 }
299             },
300             None => {
301                 post_snippet.find_uncommented(terminator)
302                             .unwrap_or(post_snippet.len())
303             }
304         };
305
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();
309
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);
316         }
317
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())
323             } else {
324                 None
325             }
326         });
327     }
328
329     result
330 }
331
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,
337     }
338 }
339
340 fn calculate_width(items: &[ListItem]) -> usize {
341     items.iter().map(total_item_width).fold(0, |a, l| a + l)
342 }
343
344 fn total_item_width(item: &ListItem) -> usize {
345     comment_len(&item.pre_comment) + comment_len(&item.post_comment) + item.item.len()
346 }
347
348 fn comment_len(comment: &Option<String>) -> usize {
349     match comment {
350         &Some(ref s) => {
351             let text_len = s.trim().len();
352             if text_len > 0 {
353                 // We'll put " /*" before and " */" after inline comments.
354                 text_len + 6
355             } else {
356                 text_len
357             }
358         },
359         &None => 0
360     }
361 }