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