]> git.lizzy.rs Git - rust.git/blob - src/lists.rs
Format tuple-like structs
[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             let width = formatting.v_width - item_width - 1; // Space between item and comment
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.is_expression && last ||
211                               comment.trim().contains('\n') ||
212                               comment.trim().len() > width;
213
214             let formatted_comment = rewrite_comment(comment,
215                                                     block_style,
216                                                     width,
217                                                     offset);
218
219             result.push(' ');
220             result.push_str(&formatted_comment);
221         }
222     }
223
224     result
225 }
226
227 fn has_line_pre_comment(item: &ListItem) -> bool {
228     match item.pre_comment {
229         Some(ref comment) => comment.starts_with("//"),
230         None => false
231     }
232 }
233
234 // Turns a list into a vector of items with associated comments.
235 // TODO: we probably do not want to take a terminator any more. Instead, we
236 // should demand a proper span end.
237 pub fn itemize_list<T, I, F1, F2, F3>(codemap: &CodeMap,
238                                       prefix: Vec<ListItem>,
239                                       it: I,
240                                       separator: &str,
241                                       terminator: &str,
242                                       get_lo: F1,
243                                       get_hi: F2,
244                                       get_item: F3,
245                                       mut prev_span_end: BytePos,
246                                       next_span_start: BytePos)
247     -> Vec<ListItem>
248     where I: Iterator<Item=T>,
249           F1: Fn(&T) -> BytePos,
250           F2: Fn(&T) -> BytePos,
251           F3: Fn(&T) -> String
252 {
253     let mut result = prefix;
254     let mut new_it = it.peekable();
255     let white_space: &[_] = &[' ', '\t'];
256
257     while let Some(item) = new_it.next() {
258         // Pre-comment
259         let pre_snippet = codemap.span_to_snippet(codemap::mk_sp(prev_span_end,
260                                                                 get_lo(&item)))
261                                  .unwrap();
262         let pre_snippet = pre_snippet.trim();
263         let pre_comment = if pre_snippet.len() > 0 {
264             Some(pre_snippet.to_owned())
265         } else {
266             None
267         };
268
269         // Post-comment
270         let next_start = match new_it.peek() {
271             Some(ref next_item) => get_lo(next_item),
272             None => next_span_start
273         };
274         let post_snippet = codemap.span_to_snippet(codemap::mk_sp(get_hi(&item),
275                                                                   next_start))
276                                   .unwrap();
277
278         let comment_end = match new_it.peek() {
279             Some(..) => {
280                 if let Some(start) = before(&post_snippet, "/*", "\n") {
281                     // Block-style post-comment. Either before or after the separator.
282                     cmp::max(find_comment_end(&post_snippet[start..]).unwrap() + start,
283                              post_snippet.find_uncommented(separator).unwrap() + separator.len())
284                 } else if let Some(idx) = post_snippet.find('\n') {
285                     idx + 1
286                 } else {
287                     post_snippet.len()
288                 }
289             },
290             None => {
291                 post_snippet.find_uncommented(terminator)
292                             .unwrap_or(post_snippet.len())
293             }
294         };
295
296         prev_span_end = get_hi(&item) + BytePos(comment_end as u32);
297         let mut post_snippet = post_snippet[..comment_end].trim();
298
299         if post_snippet.starts_with(separator) {
300             post_snippet = post_snippet[separator.len()..]
301                 .trim_matches(white_space);
302         } else if post_snippet.ends_with(separator) {
303             post_snippet = post_snippet[..post_snippet.len()-separator.len()]
304                 .trim_matches(white_space);
305         }
306
307         result.push(ListItem {
308             pre_comment: pre_comment,
309             item: get_item(&item),
310             post_comment: if post_snippet.len() > 0 {
311                 Some(post_snippet.to_owned())
312             } else {
313                 None
314             }
315         });
316     }
317
318     result
319 }
320
321 fn needs_trailing_separator(separator_tactic: SeparatorTactic, list_tactic: ListTactic) -> bool {
322     match separator_tactic {
323         SeparatorTactic::Always => true,
324         SeparatorTactic::Vertical => list_tactic == ListTactic::Vertical,
325         SeparatorTactic::Never => false,
326     }
327 }
328
329 fn calculate_width(items: &[ListItem]) -> usize {
330     items.iter().map(total_item_width).fold(0, |a, l| a + l)
331 }
332
333 fn total_item_width(item: &ListItem) -> usize {
334     comment_len(&item.pre_comment) + comment_len(&item.post_comment) + item.item.len()
335 }
336
337 fn comment_len(comment: &Option<String>) -> usize {
338     match comment {
339         &Some(ref s) => {
340             let text_len = s.trim().len();
341             if text_len > 0 {
342                 // We'll put " /*" before and " */" after inline comments.
343                 text_len + 6
344             } else {
345                 text_len
346             }
347         },
348         &None => 0
349     }
350 }