]> git.lizzy.rs Git - rust.git/blob - src/lists.rs
Format comments in struct literals
[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 is_expression: 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                                              1000,
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             // We'll assume it'll fit on one line at this point
192             let formatted_comment = rewrite_comment(item.post_comment.as_ref().unwrap(),
193                                                     true,
194                                                     1000,
195                                                     0);
196
197             result.push(' ');
198             result.push_str(&formatted_comment);
199         }
200
201         if separate {
202             result.push_str(formatting.separator);
203         }
204
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;
214
215             let formatted_comment = rewrite_comment(comment,
216                                                     block_style,
217                                                     width,
218                                                     offset);
219
220             result.push(' ');
221             result.push_str(&formatted_comment);
222         }
223     }
224
225     result
226 }
227
228 fn has_line_pre_comment(item: &ListItem) -> bool {
229     match item.pre_comment {
230         Some(ref comment) => comment.starts_with("//"),
231         None => false
232     }
233 }
234
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>,
240                                       it: I,
241                                       separator: &str,
242                                       terminator: &str,
243                                       get_lo: F1,
244                                       get_hi: F2,
245                                       get_item_string: F3,
246                                       mut prev_span_end: BytePos,
247                                       next_span_start: BytePos)
248     -> Vec<ListItem>
249     where I: Iterator<Item=T>,
250           F1: Fn(&T) -> BytePos,
251           F2: Fn(&T) -> BytePos,
252           F3: Fn(&T) -> String
253 {
254     let mut result = prefix;
255     let mut new_it = it.peekable();
256     let white_space: &[_] = &[' ', '\t'];
257
258     while let Some(item) = new_it.next() {
259         // Pre-comment
260         let pre_snippet = codemap.span_to_snippet(codemap::mk_sp(prev_span_end,
261                                                                 get_lo(&item)))
262                                  .unwrap();
263         let pre_snippet = pre_snippet.trim();
264         let pre_comment = if pre_snippet.len() > 0 {
265             Some(pre_snippet.to_owned())
266         } else {
267             None
268         };
269
270         // Post-comment
271         let next_start = match new_it.peek() {
272             Some(ref next_item) => get_lo(next_item),
273             None => next_span_start
274         };
275         let post_snippet = codemap.span_to_snippet(codemap::mk_sp(get_hi(&item),
276                                                                   next_start))
277                                   .unwrap();
278
279         let comment_end = match new_it.peek() {
280             Some(..) => {
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') {
286                     idx + 1
287                 } else {
288                     post_snippet.len()
289                 }
290             },
291             None => {
292                 post_snippet.find_uncommented(terminator)
293                             .unwrap_or(post_snippet.len())
294             }
295         };
296
297         prev_span_end = get_hi(&item) + BytePos(comment_end as u32);
298         let mut post_snippet = post_snippet[..comment_end].trim();
299
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);
306         }
307
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())
313             } else {
314                 None
315             }
316         });
317     }
318
319     result
320 }
321
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,
327     }
328 }
329
330 fn calculate_width(items: &[ListItem]) -> usize {
331     items.iter().map(total_item_width).fold(0, |a, l| a + l)
332 }
333
334 fn total_item_width(item: &ListItem) -> usize {
335     comment_len(&item.pre_comment) + comment_len(&item.post_comment) + item.item.len()
336 }
337
338 fn comment_len(comment: &Option<String>) -> usize {
339     match comment {
340         &Some(ref s) => {
341             let text_len = s.trim().len();
342             if text_len > 0 {
343                 // We'll put " /*" before and " */" after inline comments.
344                 text_len + 6
345             } else {
346                 text_len
347             }
348         },
349         &None => 0
350     }
351 }