]> git.lizzy.rs Git - rust.git/blob - src/lists.rs
Fixup comment wrapping in lists
[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 use string::before;
18
19 #[derive(Eq, PartialEq, Debug, Copy, Clone)]
20 pub enum ListTactic {
21     // One item per row.
22     Vertical,
23     // All items on one row.
24     Horizontal,
25     // Try Horizontal layout, if that fails then vertical
26     HorizontalVertical,
27     // Pack as many items as possible per row over (possibly) many rows.
28     Mixed,
29 }
30
31 #[derive(Eq, PartialEq, Debug, Copy, Clone)]
32 pub enum SeparatorTactic {
33     Always,
34     Never,
35     Vertical,
36 }
37
38 impl_enum_decodable!(SeparatorTactic, Always, Never, Vertical);
39
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,
45     pub indent: usize,
46     // Available width if we layout horizontally.
47     pub h_width: usize,
48     // Available width if we layout vertically
49     pub v_width: usize,
50     // Non-expressions, e.g. items, will have a new line at the end of the list.
51     // Important for comment styles.
52     pub ends_with_newline: bool
53 }
54
55 pub struct ListItem {
56     pub pre_comment: Option<String>,
57     // Item should include attributes and doc comments
58     pub item: String,
59     pub post_comment: Option<String>
60 }
61
62 impl ListItem {
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)
67     }
68
69     pub fn from_str<S: Into<String>>(s: S) -> ListItem {
70         ListItem {
71             pre_comment: None,
72             item: s.into(),
73             post_comment: None
74         }
75     }
76 }
77
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 {
82     if items.len() == 0 {
83         return String::new();
84     }
85
86     let mut tactic = formatting.tactic;
87
88     // Conservatively overestimates because of the changing separator tactic.
89     let sep_count = if formatting.trailing_separator != SeparatorTactic::Never {
90         items.len()
91     } else {
92         items.len() - 1
93     };
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;
98
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
106         } else {
107             ListTactic::Vertical
108         };
109     }
110
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;
115     }
116
117     // Switch to vertical mode if we find non-block comments.
118     if items.iter().any(has_line_pre_comment) {
119         tactic = ListTactic::Vertical;
120     }
121
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);
125
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
130     } else {
131         total_width + items.len() * (formatting.indent + 1)
132     };
133     let mut result = String::with_capacity(round_up_to_power_of_two(alloc_width));
134
135     let mut line_len = 0;
136     let indent_str = &make_indent(formatting.indent);
137     for (i, item) in items.iter().enumerate() {
138         let first = i == 0;
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;
143
144         match tactic {
145             ListTactic::Horizontal if !first => {
146                 result.push(' ');
147             }
148             ListTactic::Vertical if !first => {
149                 result.push('\n');
150                 result.push_str(indent_str);
151             }
152             ListTactic::Mixed => {
153                 let total_width = total_item_width(item) + item_sep_len;
154
155                 if line_len > 0 && line_len + total_width > formatting.v_width {
156                     result.push('\n');
157                     result.push_str(indent_str);
158                     line_len = 0;
159                 }
160
161                 if line_len > 0 {
162                     result.push(' ');
163                     line_len += 1;
164                 }
165
166                 line_len += total_width;
167             }
168             _ => {}
169         }
170
171         // Pre-comments
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,
176                                              // Width restriction is only
177                                              // relevant in vertical mode.
178                                              formatting.v_width,
179                                              formatting.indent));
180
181             if tactic == ListTactic::Vertical {
182                 result.push('\n');
183                 result.push_str(indent_str);
184             } else {
185                 result.push(' ');
186             }
187         }
188
189         result.push_str(&item.item);
190
191         // Post-comments
192         if tactic != ListTactic::Vertical && item.post_comment.is_some() {
193             let formatted_comment = rewrite_comment(item.post_comment.as_ref().unwrap(),
194                                                     true,
195                                                     formatting.v_width,
196                                                     0);
197
198             result.push(' ');
199             result.push_str(&formatted_comment);
200         }
201
202         if separate {
203             result.push_str(formatting.separator);
204         }
205
206         if tactic == ListTactic::Vertical && item.post_comment.is_some() {
207             // 1 = space between item and comment.
208             let width = formatting.v_width.checked_sub(item_width + 1).unwrap_or(1);
209             let offset = formatting.indent + item_width + 1;
210             let comment = item.post_comment.as_ref().unwrap();
211             // Use block-style only for the last item or multiline comments.
212             let block_style = formatting.ends_with_newline && last ||
213                               comment.trim().contains('\n') ||
214                               comment.trim().len() > width;
215
216             let formatted_comment = rewrite_comment(comment, block_style, width, offset);
217
218             result.push(' ');
219             result.push_str(&formatted_comment);
220         }
221     }
222
223     result
224 }
225
226 fn has_line_pre_comment(item: &ListItem) -> bool {
227     match item.pre_comment {
228         Some(ref comment) => comment.starts_with("//"),
229         None => false
230     }
231 }
232
233 // Turns a list into a vector of items with associated comments.
234 // TODO: we probably do not want to take a terminator any more. Instead, we
235 // should demand a proper span end.
236 pub fn itemize_list<T, I, F1, F2, F3>(codemap: &CodeMap,
237                                       prefix: Vec<ListItem>,
238                                       it: I,
239                                       separator: &str,
240                                       terminator: &str,
241                                       get_lo: F1,
242                                       get_hi: F2,
243                                       get_item_string: F3,
244                                       mut prev_span_end: BytePos,
245                                       next_span_start: BytePos)
246     -> Vec<ListItem>
247     where I: Iterator<Item=T>,
248           F1: Fn(&T) -> BytePos,
249           F2: Fn(&T) -> BytePos,
250           F3: Fn(&T) -> String
251 {
252     let mut result = prefix;
253     let mut new_it = it.peekable();
254     let white_space: &[_] = &[' ', '\t'];
255
256     while let Some(item) = new_it.next() {
257         // Pre-comment
258         let pre_snippet = codemap.span_to_snippet(codemap::mk_sp(prev_span_end,
259                                                                 get_lo(&item)))
260                                  .unwrap();
261         let pre_snippet = pre_snippet.trim();
262         let pre_comment = if pre_snippet.len() > 0 {
263             Some(pre_snippet.to_owned())
264         } else {
265             None
266         };
267
268         // Post-comment
269         let next_start = match new_it.peek() {
270             Some(ref next_item) => get_lo(next_item),
271             None => next_span_start
272         };
273         let post_snippet = codemap.span_to_snippet(codemap::mk_sp(get_hi(&item),
274                                                                   next_start))
275                                   .unwrap();
276
277         let comment_end = match new_it.peek() {
278             Some(..) => {
279                 if let Some(start) = before(&post_snippet, "/*", "\n") {
280                     // Block-style post-comment. Either before or after the separator.
281                     cmp::max(find_comment_end(&post_snippet[start..]).unwrap() + start,
282                              post_snippet.find_uncommented(separator).unwrap() + separator.len())
283                 } else if let Some(idx) = post_snippet.find('\n') {
284                     idx + 1
285                 } else {
286                     post_snippet.len()
287                 }
288             },
289             None => {
290                 post_snippet.find_uncommented(terminator)
291                             .unwrap_or(post_snippet.len())
292             }
293         };
294
295         prev_span_end = get_hi(&item) + BytePos(comment_end as u32);
296         let mut post_snippet = post_snippet[..comment_end].trim();
297
298         if post_snippet.starts_with(separator) {
299             post_snippet = post_snippet[separator.len()..]
300                 .trim_matches(white_space);
301         } else if post_snippet.ends_with(separator) {
302             post_snippet = post_snippet[..post_snippet.len()-separator.len()]
303                 .trim_matches(white_space);
304         }
305
306         result.push(ListItem {
307             pre_comment: pre_comment,
308             item: get_item_string(&item),
309             post_comment: if post_snippet.len() > 0 {
310                 Some(post_snippet.to_owned())
311             } else {
312                 None
313             }
314         });
315     }
316
317     result
318 }
319
320 fn needs_trailing_separator(separator_tactic: SeparatorTactic, list_tactic: ListTactic) -> bool {
321     match separator_tactic {
322         SeparatorTactic::Always => true,
323         SeparatorTactic::Vertical => list_tactic == ListTactic::Vertical,
324         SeparatorTactic::Never => false,
325     }
326 }
327
328 fn calculate_width(items: &[ListItem]) -> usize {
329     items.iter().map(total_item_width).fold(0, |a, l| a + l)
330 }
331
332 fn total_item_width(item: &ListItem) -> usize {
333     comment_len(&item.pre_comment) + comment_len(&item.post_comment) + item.item.len()
334 }
335
336 fn comment_len(comment: &Option<String>) -> usize {
337     match comment {
338         &Some(ref s) => {
339             let text_len = s.trim().len();
340             if text_len > 0 {
341                 // We'll put " /*" before and " */" after inline comments.
342                 text_len + 6
343             } else {
344                 text_len
345             }
346         },
347         &None => 0
348     }
349 }