]> git.lizzy.rs Git - rust.git/blob - src/lists.rs
Remove unnecessary config parameter from format_missing_with_indent.
[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 use std::iter::Peekable;
13
14 use syntax::codemap::{self, CodeMap, BytePos};
15
16 use Indent;
17 use utils::{round_up_to_power_of_two, wrap_str};
18 use comment::{FindUncommented, rewrite_comment, find_comment_end};
19 use config::Config;
20
21 #[derive(Eq, PartialEq, Debug, Copy, Clone)]
22 pub enum ListTactic {
23     // One item per row.
24     Vertical,
25     // All items on one row.
26     Horizontal,
27     // Try Horizontal layout, if that fails then vertical
28     HorizontalVertical,
29     // Pack as many items as possible per row over (possibly) many rows.
30     Mixed,
31 }
32
33 impl_enum_decodable!(ListTactic, Vertical, Horizontal, HorizontalVertical, Mixed);
34
35 #[derive(Eq, PartialEq, Debug, Copy, Clone)]
36 pub enum SeparatorTactic {
37     Always,
38     Never,
39     Vertical,
40 }
41
42 impl_enum_decodable!(SeparatorTactic, Always, Never, Vertical);
43
44 // TODO having some helpful ctors for ListFormatting would be nice.
45 pub struct ListFormatting<'a> {
46     pub tactic: ListTactic,
47     pub separator: &'a str,
48     pub trailing_separator: SeparatorTactic,
49     pub indent: Indent,
50     // Available width if we layout horizontally.
51     pub h_width: usize,
52     // Available width if we layout vertically
53     pub v_width: usize,
54     // Non-expressions, e.g. items, will have a new line at the end of the list.
55     // Important for comment styles.
56     pub ends_with_newline: bool,
57     pub config: &'a Config,
58 }
59
60 impl<'a> ListFormatting<'a> {
61     pub fn for_fn(width: usize, offset: Indent, config: &'a Config) -> ListFormatting<'a> {
62         ListFormatting {
63             tactic: ListTactic::HorizontalVertical,
64             separator: ",",
65             trailing_separator: SeparatorTactic::Never,
66             indent: offset,
67             h_width: width,
68             v_width: width,
69             ends_with_newline: false,
70             config: config,
71         }
72     }
73 }
74
75 pub struct ListItem {
76     pub pre_comment: Option<String>,
77     // Item should include attributes and doc comments.
78     pub item: String,
79     pub post_comment: Option<String>,
80     // Whether there is extra whitespace before this item.
81     pub new_lines: bool,
82 }
83
84 impl ListItem {
85     pub fn is_multiline(&self) -> bool {
86         self.item.contains('\n') || self.pre_comment.is_some() ||
87         self.post_comment.as_ref().map(|s| s.contains('\n')).unwrap_or(false)
88     }
89
90     pub fn has_line_pre_comment(&self) -> bool {
91         self.pre_comment.as_ref().map_or(false, |comment| comment.starts_with("//"))
92     }
93
94     pub fn from_str<S: Into<String>>(s: S) -> ListItem {
95         ListItem { pre_comment: None, item: s.into(), post_comment: None, new_lines: false }
96     }
97 }
98
99 // Format a list of commented items into a string.
100 // FIXME: this has grown into a monstrosity
101 // TODO: add unit tests
102 pub fn write_list<'b>(items: &[ListItem], formatting: &ListFormatting<'b>) -> Option<String> {
103     if items.is_empty() {
104         return Some(String::new());
105     }
106
107     let mut tactic = formatting.tactic;
108
109     // Conservatively overestimates because of the changing separator tactic.
110     let sep_count = if formatting.trailing_separator == SeparatorTactic::Always {
111         items.len()
112     } else {
113         items.len() - 1
114     };
115     let sep_len = formatting.separator.len();
116     let total_sep_len = (sep_len + 1) * sep_count;
117     let total_width = calculate_width(items);
118     let fits_single = total_width + total_sep_len <= formatting.h_width;
119
120     // Check if we need to fallback from horizontal listing, if possible.
121     if tactic == ListTactic::HorizontalVertical {
122         debug!("write_list: total_width: {}, total_sep_len: {}, h_width: {}",
123                total_width,
124                total_sep_len,
125                formatting.h_width);
126         tactic = if fits_single && !items.iter().any(ListItem::is_multiline) {
127             ListTactic::Horizontal
128         } else {
129             ListTactic::Vertical
130         };
131     }
132
133     // Check if we can fit everything on a single line in mixed mode.
134     // The horizontal tactic does not break after v_width columns.
135     if tactic == ListTactic::Mixed && fits_single {
136         tactic = ListTactic::Horizontal;
137     }
138
139     // Switch to vertical mode if we find non-block comments.
140     if items.iter().any(ListItem::has_line_pre_comment) {
141         tactic = ListTactic::Vertical;
142     }
143
144     // Now that we know how we will layout, we can decide for sure if there
145     // will be a trailing separator.
146     let trailing_separator = needs_trailing_separator(formatting.trailing_separator, tactic);
147
148     // Create a buffer for the result.
149     // TODO could use a StringBuffer or rope for this
150     let alloc_width = if tactic == ListTactic::Horizontal {
151         total_width + total_sep_len
152     } else {
153         total_width + items.len() * (formatting.indent.width() + 1)
154     };
155     let mut result = String::with_capacity(round_up_to_power_of_two(alloc_width));
156
157     let mut line_len = 0;
158     let indent_str = &formatting.indent.to_string(formatting.config);
159     for (i, item) in items.iter().enumerate() {
160         let first = i == 0;
161         let last = i == items.len() - 1;
162         let separate = !last || trailing_separator;
163         let item_sep_len = if separate {
164             sep_len
165         } else {
166             0
167         };
168         let item_width = item.item.len() + item_sep_len;
169
170         match tactic {
171             ListTactic::Horizontal if !first => {
172                 result.push(' ');
173             }
174             ListTactic::Vertical if !first => {
175                 result.push('\n');
176                 result.push_str(indent_str);
177             }
178             ListTactic::Mixed => {
179                 let total_width = total_item_width(item) + item_sep_len;
180
181                 if line_len > 0 && line_len + total_width > formatting.v_width {
182                     result.push('\n');
183                     result.push_str(indent_str);
184                     line_len = 0;
185                 }
186
187                 if line_len > 0 {
188                     result.push(' ');
189                     line_len += 1;
190                 }
191
192                 line_len += total_width;
193             }
194             _ => {}
195         }
196
197         // Pre-comments
198         if let Some(ref comment) = item.pre_comment {
199             // Block style in non-vertical mode.
200             let block_mode = tactic != ListTactic::Vertical;
201             // Width restriction is only relevant in vertical mode.
202             let max_width = formatting.v_width;
203             result.push_str(&rewrite_comment(comment, block_mode, max_width, formatting.indent,
204                                              formatting.config));
205
206             if tactic == ListTactic::Vertical {
207                 result.push('\n');
208                 result.push_str(indent_str);
209             } else {
210                 result.push(' ');
211             }
212         }
213
214         let max_width = formatting.indent.width() + formatting.v_width;
215         let item_str = wrap_str(&item.item[..], max_width, formatting.v_width, formatting.indent);
216         result.push_str(&&try_opt!(item_str));
217
218         // Post-comments
219         if tactic != ListTactic::Vertical && item.post_comment.is_some() {
220             let comment = item.post_comment.as_ref().unwrap();
221             let formatted_comment = rewrite_comment(comment,
222                                                     true,
223                                                     formatting.v_width,
224                                                     Indent::empty(),
225                                                     formatting.config);
226
227             result.push(' ');
228             result.push_str(&formatted_comment);
229         }
230
231         if separate {
232             result.push_str(formatting.separator);
233         }
234
235         if tactic == ListTactic::Vertical && item.post_comment.is_some() {
236             // 1 = space between item and comment.
237             let width = formatting.v_width.checked_sub(item_width + 1).unwrap_or(1);
238             let mut offset = formatting.indent;
239             offset.alignment += item_width + 1;
240             let comment = item.post_comment.as_ref().unwrap();
241             // Use block-style only for the last item or multiline comments.
242             let block_style = !formatting.ends_with_newline && last ||
243                               comment.trim().contains('\n') ||
244                               comment.trim().len() > width;
245
246             let formatted_comment = rewrite_comment(comment,
247                                                     block_style,
248                                                     width,
249                                                     offset,
250                                                     formatting.config);
251
252             result.push(' ');
253             result.push_str(&formatted_comment);
254         }
255
256         if !last && tactic == ListTactic::Vertical && item.new_lines {
257             result.push('\n');
258         }
259     }
260
261     Some(result)
262 }
263
264 pub struct ListItems<'a, I, F1, F2, F3>
265     where I: Iterator
266 {
267     codemap: &'a CodeMap,
268     inner: Peekable<I>,
269     get_lo: F1,
270     get_hi: F2,
271     get_item_string: F3,
272     prev_span_end: BytePos,
273     next_span_start: BytePos,
274     terminator: &'a str,
275 }
276
277 impl<'a, T, I, F1, F2, F3> Iterator for ListItems<'a, I, F1, F2, F3>
278     where I: Iterator<Item = T>,
279           F1: Fn(&T) -> BytePos,
280           F2: Fn(&T) -> BytePos,
281           F3: Fn(&T) -> String
282 {
283     type Item = ListItem;
284
285     fn next(&mut self) -> Option<Self::Item> {
286         let white_space: &[_] = &[' ', '\t'];
287
288         self.inner.next().map(|item| {
289             let mut new_lines = false;
290             // Pre-comment
291             let pre_snippet = self.codemap
292                                   .span_to_snippet(codemap::mk_sp(self.prev_span_end,
293                                                                   (self.get_lo)(&item)))
294                                   .unwrap();
295             let trimmed_pre_snippet = pre_snippet.trim();
296             let pre_comment = if !trimmed_pre_snippet.is_empty() {
297                 Some(trimmed_pre_snippet.to_owned())
298             } else {
299                 None
300             };
301
302             // Post-comment
303             let next_start = match self.inner.peek() {
304                 Some(ref next_item) => (self.get_lo)(next_item),
305                 None => self.next_span_start,
306             };
307             let post_snippet = self.codemap
308                                    .span_to_snippet(codemap::mk_sp((self.get_hi)(&item),
309                                                                    next_start))
310                                    .unwrap();
311
312             let comment_end = match self.inner.peek() {
313                 Some(..) => {
314                     let block_open_index = post_snippet.find("/*");
315                     let newline_index = post_snippet.find('\n');
316                     let separator_index = post_snippet.find_uncommented(",").unwrap();
317
318                     match (block_open_index, newline_index) {
319                         // Separator before comment, with the next item on same line.
320                         // Comment belongs to next item.
321                         (Some(i), None) if i > separator_index => {
322                             separator_index + 1
323                         }
324                         // Block-style post-comment before the separator.
325                         (Some(i), None) => {
326                             cmp::max(find_comment_end(&post_snippet[i..]).unwrap() + i,
327                                      separator_index + 1)
328                         }
329                         // Block-style post-comment. Either before or after the separator.
330                         (Some(i), Some(j)) if i < j => {
331                             cmp::max(find_comment_end(&post_snippet[i..]).unwrap() + i,
332                                      separator_index + 1)
333                         }
334                         // Potential *single* line comment.
335                         (_, Some(j)) => j + 1,
336                         _ => post_snippet.len(),
337                     }
338                 }
339                 None => {
340                     post_snippet.find_uncommented(self.terminator).unwrap_or(post_snippet.len())
341                 }
342             };
343
344             if !post_snippet.is_empty() && comment_end > 0 {
345                 // Account for extra whitespace between items. This is fiddly
346                 // because of the way we divide pre- and post- comments.
347
348                 // Everything from the separator to the next item.
349                 let test_snippet = &post_snippet[comment_end-1..];
350                 let first_newline = test_snippet.find('\n').unwrap_or(test_snippet.len());
351                 // From the end of the first line of comments.
352                 let test_snippet = &test_snippet[first_newline..];
353                 let first = test_snippet.find(|c: char| !c.is_whitespace())
354                                         .unwrap_or(test_snippet.len());
355                 // From the end of the first line of comments to the next non-whitespace char.
356                 let test_snippet = &test_snippet[..first];
357
358                 if test_snippet.chars().filter(|c| c == &'\n').count() > 1 {
359                     // There were multiple line breaks which got trimmed to nothing.
360                     new_lines = true;
361                 }
362             }
363
364             // Cleanup post-comment: strip separators and whitespace.
365             self.prev_span_end = (self.get_hi)(&item) + BytePos(comment_end as u32);
366             let post_snippet = post_snippet[..comment_end].trim();
367
368             let post_snippet_trimmed = if post_snippet.starts_with(',') {
369                 post_snippet[1..].trim_matches(white_space)
370             } else if post_snippet.ends_with(",") {
371                 post_snippet[..(post_snippet.len() - 1)].trim_matches(white_space)
372             } else {
373                 post_snippet
374             };
375
376             let post_comment = if !post_snippet_trimmed.is_empty() {
377                 Some(post_snippet_trimmed.to_owned())
378             } else {
379                 None
380             };
381
382             ListItem {
383                 pre_comment: pre_comment,
384                 item: (self.get_item_string)(&item),
385                 post_comment: post_comment,
386                 new_lines: new_lines,
387             }
388         })
389     }
390 }
391
392 // Creates an iterator over a list's items with associated comments.
393 pub fn itemize_list<'a, T, I, F1, F2, F3>(codemap: &'a CodeMap,
394                                           inner: I,
395                                           terminator: &'a str,
396                                           get_lo: F1,
397                                           get_hi: F2,
398                                           get_item_string: F3,
399                                           prev_span_end: BytePos,
400                                           next_span_start: BytePos)
401                                           -> ListItems<'a, I, F1, F2, F3>
402     where I: Iterator<Item = T>,
403           F1: Fn(&T) -> BytePos,
404           F2: Fn(&T) -> BytePos,
405           F3: Fn(&T) -> String
406 {
407     ListItems {
408         codemap: codemap,
409         inner: inner.peekable(),
410         get_lo: get_lo,
411         get_hi: get_hi,
412         get_item_string: get_item_string,
413         prev_span_end: prev_span_end,
414         next_span_start: next_span_start,
415         terminator: terminator,
416     }
417 }
418
419 fn needs_trailing_separator(separator_tactic: SeparatorTactic, list_tactic: ListTactic) -> bool {
420     match separator_tactic {
421         SeparatorTactic::Always => true,
422         SeparatorTactic::Vertical => list_tactic == ListTactic::Vertical,
423         SeparatorTactic::Never => false,
424     }
425 }
426
427 fn calculate_width(items: &[ListItem]) -> usize {
428     items.iter().map(total_item_width).fold(0, |a, l| a + l)
429 }
430
431 fn total_item_width(item: &ListItem) -> usize {
432     comment_len(&item.pre_comment) + comment_len(&item.post_comment) + item.item.len()
433 }
434
435 fn comment_len(comment: &Option<String>) -> usize {
436     match *comment {
437         Some(ref s) => {
438             let text_len = s.trim().len();
439             if text_len > 0 {
440                 // We'll put " /*" before and " */" after inline comments.
441                 text_len + 6
442             } else {
443                 text_len
444             }
445         }
446         None => 0,
447     }
448 }