]>
Commit | Line | Data |
---|---|---|
f20569fa XL |
1 | //! Format list-like expressions and items. |
2 | ||
3 | use std::cmp; | |
4 | use std::iter::Peekable; | |
5 | ||
6 | use rustc_span::BytePos; | |
7 | ||
8 | use crate::comment::{find_comment_end, rewrite_comment, FindUncommented}; | |
9 | use crate::config::lists::*; | |
10 | use crate::config::{Config, IndentStyle}; | |
11 | use crate::rewrite::RewriteContext; | |
12 | use crate::shape::{Indent, Shape}; | |
13 | use crate::utils::{ | |
14 | count_newlines, first_line_width, last_line_width, mk_sp, starts_with_newline, | |
15 | unicode_str_width, | |
16 | }; | |
17 | use crate::visitor::SnippetProvider; | |
18 | ||
19 | pub(crate) struct ListFormatting<'a> { | |
20 | tactic: DefinitiveListTactic, | |
21 | separator: &'a str, | |
22 | trailing_separator: SeparatorTactic, | |
23 | separator_place: SeparatorPlace, | |
24 | shape: Shape, | |
25 | // Non-expressions, e.g., items, will have a new line at the end of the list. | |
26 | // Important for comment styles. | |
27 | ends_with_newline: bool, | |
28 | // Remove newlines between list elements for expressions. | |
29 | preserve_newline: bool, | |
30 | // Nested import lists get some special handling for the "Mixed" list type | |
31 | nested: bool, | |
32 | // Whether comments should be visually aligned. | |
33 | align_comments: bool, | |
34 | config: &'a Config, | |
35 | } | |
36 | ||
37 | impl<'a> ListFormatting<'a> { | |
38 | pub(crate) fn new(shape: Shape, config: &'a Config) -> Self { | |
39 | ListFormatting { | |
40 | tactic: DefinitiveListTactic::Vertical, | |
41 | separator: ",", | |
42 | trailing_separator: SeparatorTactic::Never, | |
43 | separator_place: SeparatorPlace::Back, | |
44 | shape, | |
45 | ends_with_newline: true, | |
46 | preserve_newline: false, | |
47 | nested: false, | |
48 | align_comments: true, | |
49 | config, | |
50 | } | |
51 | } | |
52 | ||
53 | pub(crate) fn tactic(mut self, tactic: DefinitiveListTactic) -> Self { | |
54 | self.tactic = tactic; | |
55 | self | |
56 | } | |
57 | ||
58 | pub(crate) fn separator(mut self, separator: &'a str) -> Self { | |
59 | self.separator = separator; | |
60 | self | |
61 | } | |
62 | ||
63 | pub(crate) fn trailing_separator(mut self, trailing_separator: SeparatorTactic) -> Self { | |
64 | self.trailing_separator = trailing_separator; | |
65 | self | |
66 | } | |
67 | ||
68 | pub(crate) fn separator_place(mut self, separator_place: SeparatorPlace) -> Self { | |
69 | self.separator_place = separator_place; | |
70 | self | |
71 | } | |
72 | ||
73 | pub(crate) fn ends_with_newline(mut self, ends_with_newline: bool) -> Self { | |
74 | self.ends_with_newline = ends_with_newline; | |
75 | self | |
76 | } | |
77 | ||
78 | pub(crate) fn preserve_newline(mut self, preserve_newline: bool) -> Self { | |
79 | self.preserve_newline = preserve_newline; | |
80 | self | |
81 | } | |
82 | ||
83 | pub(crate) fn nested(mut self, nested: bool) -> Self { | |
84 | self.nested = nested; | |
85 | self | |
86 | } | |
87 | ||
88 | pub(crate) fn align_comments(mut self, align_comments: bool) -> Self { | |
89 | self.align_comments = align_comments; | |
90 | self | |
91 | } | |
92 | ||
93 | pub(crate) fn needs_trailing_separator(&self) -> bool { | |
94 | match self.trailing_separator { | |
95 | // We always put separator in front. | |
96 | SeparatorTactic::Always => true, | |
97 | SeparatorTactic::Vertical => self.tactic == DefinitiveListTactic::Vertical, | |
98 | SeparatorTactic::Never => { | |
99 | self.tactic == DefinitiveListTactic::Vertical && self.separator_place.is_front() | |
100 | } | |
101 | } | |
102 | } | |
103 | } | |
104 | ||
105 | impl AsRef<ListItem> for ListItem { | |
106 | fn as_ref(&self) -> &ListItem { | |
107 | self | |
108 | } | |
109 | } | |
110 | ||
111 | #[derive(PartialEq, Eq, Debug, Copy, Clone)] | |
112 | pub(crate) enum ListItemCommentStyle { | |
113 | // Try to keep the comment on the same line with the item. | |
114 | SameLine, | |
115 | // Put the comment on the previous or the next line of the item. | |
116 | DifferentLine, | |
117 | // No comment available. | |
118 | None, | |
119 | } | |
120 | ||
121 | #[derive(Debug, Clone)] | |
122 | pub(crate) struct ListItem { | |
123 | // None for comments mean that they are not present. | |
124 | pub(crate) pre_comment: Option<String>, | |
125 | pub(crate) pre_comment_style: ListItemCommentStyle, | |
126 | // Item should include attributes and doc comments. None indicates a failed | |
127 | // rewrite. | |
128 | pub(crate) item: Option<String>, | |
129 | pub(crate) post_comment: Option<String>, | |
130 | // Whether there is extra whitespace before this item. | |
131 | pub(crate) new_lines: bool, | |
132 | } | |
133 | ||
134 | impl ListItem { | |
135 | pub(crate) fn empty() -> ListItem { | |
136 | ListItem { | |
137 | pre_comment: None, | |
138 | pre_comment_style: ListItemCommentStyle::None, | |
139 | item: None, | |
140 | post_comment: None, | |
141 | new_lines: false, | |
142 | } | |
143 | } | |
144 | ||
145 | pub(crate) fn inner_as_ref(&self) -> &str { | |
146 | self.item.as_ref().map_or("", |s| s) | |
147 | } | |
148 | ||
149 | pub(crate) fn is_different_group(&self) -> bool { | |
150 | self.inner_as_ref().contains('\n') | |
151 | || self.pre_comment.is_some() | |
152 | || self | |
153 | .post_comment | |
154 | .as_ref() | |
155 | .map_or(false, |s| s.contains('\n')) | |
156 | } | |
157 | ||
158 | pub(crate) fn is_multiline(&self) -> bool { | |
159 | self.inner_as_ref().contains('\n') | |
160 | || self | |
161 | .pre_comment | |
162 | .as_ref() | |
163 | .map_or(false, |s| s.contains('\n')) | |
164 | || self | |
165 | .post_comment | |
166 | .as_ref() | |
167 | .map_or(false, |s| s.contains('\n')) | |
168 | } | |
169 | ||
170 | pub(crate) fn has_single_line_comment(&self) -> bool { | |
171 | self.pre_comment | |
172 | .as_ref() | |
173 | .map_or(false, |comment| comment.trim_start().starts_with("//")) | |
174 | || self | |
175 | .post_comment | |
176 | .as_ref() | |
177 | .map_or(false, |comment| comment.trim_start().starts_with("//")) | |
178 | } | |
179 | ||
180 | pub(crate) fn has_comment(&self) -> bool { | |
181 | self.pre_comment.is_some() || self.post_comment.is_some() | |
182 | } | |
183 | ||
184 | pub(crate) fn from_str<S: Into<String>>(s: S) -> ListItem { | |
185 | ListItem { | |
186 | pre_comment: None, | |
187 | pre_comment_style: ListItemCommentStyle::None, | |
188 | item: Some(s.into()), | |
189 | post_comment: None, | |
190 | new_lines: false, | |
191 | } | |
192 | } | |
193 | ||
194 | // Returns `true` if the item causes something to be written. | |
195 | fn is_substantial(&self) -> bool { | |
196 | fn empty(s: &Option<String>) -> bool { | |
94222f64 | 197 | !matches!(*s, Some(ref s) if !s.is_empty()) |
f20569fa XL |
198 | } |
199 | ||
200 | !(empty(&self.pre_comment) && empty(&self.item) && empty(&self.post_comment)) | |
201 | } | |
202 | } | |
203 | ||
204 | /// The type of separator for lists. | |
205 | #[derive(Copy, Clone, Eq, PartialEq, Debug)] | |
206 | pub(crate) enum Separator { | |
207 | Comma, | |
208 | VerticalBar, | |
209 | } | |
210 | ||
211 | impl Separator { | |
212 | pub(crate) fn len(self) -> usize { | |
213 | match self { | |
214 | // 2 = `, ` | |
215 | Separator::Comma => 2, | |
216 | // 3 = ` | ` | |
217 | Separator::VerticalBar => 3, | |
218 | } | |
219 | } | |
220 | } | |
221 | ||
222 | pub(crate) fn definitive_tactic<I, T>( | |
223 | items: I, | |
224 | tactic: ListTactic, | |
225 | sep: Separator, | |
226 | width: usize, | |
227 | ) -> DefinitiveListTactic | |
228 | where | |
229 | I: IntoIterator<Item = T> + Clone, | |
230 | T: AsRef<ListItem>, | |
231 | { | |
232 | let pre_line_comments = items | |
233 | .clone() | |
234 | .into_iter() | |
235 | .any(|item| item.as_ref().has_single_line_comment()); | |
236 | ||
237 | let limit = match tactic { | |
238 | _ if pre_line_comments => return DefinitiveListTactic::Vertical, | |
239 | ListTactic::Horizontal => return DefinitiveListTactic::Horizontal, | |
240 | ListTactic::Vertical => return DefinitiveListTactic::Vertical, | |
241 | ListTactic::LimitedHorizontalVertical(limit) => ::std::cmp::min(width, limit), | |
242 | ListTactic::Mixed | ListTactic::HorizontalVertical => width, | |
243 | }; | |
244 | ||
245 | let (sep_count, total_width) = calculate_width(items.clone()); | |
246 | let total_sep_len = sep.len() * sep_count.saturating_sub(1); | |
247 | let real_total = total_width + total_sep_len; | |
248 | ||
249 | if real_total <= limit && !items.into_iter().any(|item| item.as_ref().is_multiline()) { | |
250 | DefinitiveListTactic::Horizontal | |
251 | } else { | |
252 | match tactic { | |
253 | ListTactic::Mixed => DefinitiveListTactic::Mixed, | |
254 | _ => DefinitiveListTactic::Vertical, | |
255 | } | |
256 | } | |
257 | } | |
258 | ||
259 | // Format a list of commented items into a string. | |
260 | pub(crate) fn write_list<I, T>(items: I, formatting: &ListFormatting<'_>) -> Option<String> | |
261 | where | |
262 | I: IntoIterator<Item = T> + Clone, | |
263 | T: AsRef<ListItem>, | |
264 | { | |
265 | let tactic = formatting.tactic; | |
266 | let sep_len = formatting.separator.len(); | |
267 | ||
268 | // Now that we know how we will layout, we can decide for sure if there | |
269 | // will be a trailing separator. | |
270 | let mut trailing_separator = formatting.needs_trailing_separator(); | |
271 | let mut result = String::with_capacity(128); | |
272 | let cloned_items = items.clone(); | |
273 | let mut iter = items.into_iter().enumerate().peekable(); | |
274 | let mut item_max_width: Option<usize> = None; | |
275 | let sep_place = | |
276 | SeparatorPlace::from_tactic(formatting.separator_place, tactic, formatting.separator); | |
277 | let mut prev_item_had_post_comment = false; | |
278 | let mut prev_item_is_nested_import = false; | |
279 | ||
280 | let mut line_len = 0; | |
281 | let indent_str = &formatting.shape.indent.to_string(formatting.config); | |
282 | while let Some((i, item)) = iter.next() { | |
283 | let item = item.as_ref(); | |
284 | let inner_item = item.item.as_ref()?; | |
285 | let first = i == 0; | |
286 | let last = iter.peek().is_none(); | |
287 | let mut separate = match sep_place { | |
288 | SeparatorPlace::Front => !first, | |
289 | SeparatorPlace::Back => !last || trailing_separator, | |
290 | }; | |
291 | let item_sep_len = if separate { sep_len } else { 0 }; | |
292 | ||
293 | // Item string may be multi-line. Its length (used for block comment alignment) | |
294 | // should be only the length of the last line. | |
295 | let item_last_line = if item.is_multiline() { | |
296 | inner_item.lines().last().unwrap_or("") | |
297 | } else { | |
298 | inner_item.as_ref() | |
299 | }; | |
300 | let mut item_last_line_width = item_last_line.len() + item_sep_len; | |
301 | if item_last_line.starts_with(&**indent_str) { | |
302 | item_last_line_width -= indent_str.len(); | |
303 | } | |
304 | ||
305 | if !item.is_substantial() { | |
306 | continue; | |
307 | } | |
308 | ||
309 | match tactic { | |
310 | DefinitiveListTactic::Horizontal if !first => { | |
311 | result.push(' '); | |
312 | } | |
313 | DefinitiveListTactic::SpecialMacro(num_args_before) => { | |
314 | if i == 0 { | |
315 | // Nothing | |
316 | } else if i < num_args_before { | |
317 | result.push(' '); | |
318 | } else if i <= num_args_before + 1 { | |
319 | result.push('\n'); | |
320 | result.push_str(indent_str); | |
321 | } else { | |
322 | result.push(' '); | |
323 | } | |
324 | } | |
325 | DefinitiveListTactic::Vertical | |
326 | if !first && !inner_item.is_empty() && !result.is_empty() => | |
327 | { | |
328 | result.push('\n'); | |
329 | result.push_str(indent_str); | |
330 | } | |
331 | DefinitiveListTactic::Mixed => { | |
332 | let total_width = total_item_width(item) + item_sep_len; | |
333 | ||
334 | // 1 is space between separator and item. | |
335 | if (line_len > 0 && line_len + 1 + total_width > formatting.shape.width) | |
336 | || prev_item_had_post_comment | |
337 | || (formatting.nested | |
338 | && (prev_item_is_nested_import || (!first && inner_item.contains("::")))) | |
339 | { | |
340 | result.push('\n'); | |
341 | result.push_str(indent_str); | |
342 | line_len = 0; | |
343 | if formatting.ends_with_newline { | |
344 | trailing_separator = true; | |
345 | } | |
346 | } else if line_len > 0 { | |
347 | result.push(' '); | |
348 | line_len += 1; | |
349 | } | |
350 | ||
351 | if last && formatting.ends_with_newline { | |
352 | separate = formatting.trailing_separator != SeparatorTactic::Never; | |
353 | } | |
354 | ||
355 | line_len += total_width; | |
356 | } | |
357 | _ => {} | |
358 | } | |
359 | ||
360 | // Pre-comments | |
361 | if let Some(ref comment) = item.pre_comment { | |
362 | // Block style in non-vertical mode. | |
363 | let block_mode = tactic == DefinitiveListTactic::Horizontal; | |
364 | // Width restriction is only relevant in vertical mode. | |
365 | let comment = | |
366 | rewrite_comment(comment, block_mode, formatting.shape, formatting.config)?; | |
367 | result.push_str(&comment); | |
368 | ||
369 | if !inner_item.is_empty() { | |
3c0e092e XL |
370 | use DefinitiveListTactic::*; |
371 | if matches!(tactic, Vertical | Mixed | SpecialMacro(_)) { | |
372 | // We cannot keep pre-comments on the same line if the comment is normalized. | |
f20569fa XL |
373 | let keep_comment = if formatting.config.normalize_comments() |
374 | || item.pre_comment_style == ListItemCommentStyle::DifferentLine | |
375 | { | |
376 | false | |
377 | } else { | |
378 | // We will try to keep the comment on the same line with the item here. | |
379 | // 1 = ` ` | |
380 | let total_width = total_item_width(item) + item_sep_len + 1; | |
381 | total_width <= formatting.shape.width | |
382 | }; | |
383 | if keep_comment { | |
384 | result.push(' '); | |
385 | } else { | |
386 | result.push('\n'); | |
387 | result.push_str(indent_str); | |
388 | // This is the width of the item (without comments). | |
3c0e092e | 389 | line_len = item.item.as_ref().map_or(0, |s| unicode_str_width(s)); |
f20569fa XL |
390 | } |
391 | } else { | |
3c0e092e | 392 | result.push(' ') |
f20569fa XL |
393 | } |
394 | } | |
395 | item_max_width = None; | |
396 | } | |
397 | ||
398 | if separate && sep_place.is_front() && !first { | |
399 | result.push_str(formatting.separator.trim()); | |
400 | result.push(' '); | |
401 | } | |
402 | result.push_str(inner_item); | |
403 | ||
404 | // Post-comments | |
405 | if tactic == DefinitiveListTactic::Horizontal && item.post_comment.is_some() { | |
406 | let comment = item.post_comment.as_ref().unwrap(); | |
407 | let formatted_comment = rewrite_comment( | |
408 | comment, | |
409 | true, | |
410 | Shape::legacy(formatting.shape.width, Indent::empty()), | |
411 | formatting.config, | |
412 | )?; | |
413 | ||
414 | result.push(' '); | |
415 | result.push_str(&formatted_comment); | |
416 | } | |
417 | ||
418 | if separate && sep_place.is_back() { | |
419 | result.push_str(formatting.separator); | |
420 | } | |
421 | ||
422 | if tactic != DefinitiveListTactic::Horizontal && item.post_comment.is_some() { | |
423 | let comment = item.post_comment.as_ref().unwrap(); | |
424 | let overhead = last_line_width(&result) + first_line_width(comment.trim()); | |
425 | ||
426 | let rewrite_post_comment = |item_max_width: &mut Option<usize>| { | |
427 | if item_max_width.is_none() && !last && !inner_item.contains('\n') { | |
428 | *item_max_width = Some(max_width_of_item_with_post_comment( | |
429 | &cloned_items, | |
430 | i, | |
431 | overhead, | |
432 | formatting.config.max_width(), | |
433 | )); | |
434 | } | |
435 | let overhead = if starts_with_newline(comment) { | |
436 | 0 | |
437 | } else if let Some(max_width) = *item_max_width { | |
438 | max_width + 2 | |
439 | } else { | |
440 | // 1 = space between item and comment. | |
441 | item_last_line_width + 1 | |
442 | }; | |
443 | let width = formatting.shape.width.checked_sub(overhead).unwrap_or(1); | |
444 | let offset = formatting.shape.indent + overhead; | |
445 | let comment_shape = Shape::legacy(width, offset); | |
446 | ||
a2a8927a XL |
447 | let block_style = if !formatting.ends_with_newline && last { |
448 | true | |
449 | } else if starts_with_newline(comment) { | |
450 | false | |
451 | } else { | |
452 | comment.trim().contains('\n') || comment.trim().len() > width | |
453 | }; | |
f20569fa XL |
454 | |
455 | rewrite_comment( | |
456 | comment.trim_start(), | |
457 | block_style, | |
458 | comment_shape, | |
459 | formatting.config, | |
460 | ) | |
461 | }; | |
462 | ||
463 | let mut formatted_comment = rewrite_post_comment(&mut item_max_width)?; | |
464 | ||
465 | if !starts_with_newline(comment) { | |
466 | if formatting.align_comments { | |
467 | let mut comment_alignment = | |
468 | post_comment_alignment(item_max_width, inner_item.len()); | |
469 | if first_line_width(&formatted_comment) | |
470 | + last_line_width(&result) | |
471 | + comment_alignment | |
472 | + 1 | |
473 | > formatting.config.max_width() | |
474 | { | |
475 | item_max_width = None; | |
476 | formatted_comment = rewrite_post_comment(&mut item_max_width)?; | |
477 | comment_alignment = | |
478 | post_comment_alignment(item_max_width, inner_item.len()); | |
479 | } | |
480 | for _ in 0..=comment_alignment { | |
481 | result.push(' '); | |
482 | } | |
483 | } | |
484 | // An additional space for the missing trailing separator (or | |
485 | // if we skipped alignment above). | |
486 | if !formatting.align_comments | |
487 | || (last | |
488 | && item_max_width.is_some() | |
489 | && !separate | |
490 | && !formatting.separator.is_empty()) | |
491 | { | |
492 | result.push(' '); | |
493 | } | |
494 | } else { | |
495 | result.push('\n'); | |
496 | result.push_str(indent_str); | |
497 | } | |
498 | if formatted_comment.contains('\n') { | |
499 | item_max_width = None; | |
500 | } | |
501 | result.push_str(&formatted_comment); | |
502 | } else { | |
503 | item_max_width = None; | |
504 | } | |
505 | ||
506 | if formatting.preserve_newline | |
507 | && !last | |
508 | && tactic == DefinitiveListTactic::Vertical | |
509 | && item.new_lines | |
510 | { | |
511 | item_max_width = None; | |
512 | result.push('\n'); | |
513 | } | |
514 | ||
515 | prev_item_had_post_comment = item.post_comment.is_some(); | |
516 | prev_item_is_nested_import = inner_item.contains("::"); | |
517 | } | |
518 | ||
519 | Some(result) | |
520 | } | |
521 | ||
522 | fn max_width_of_item_with_post_comment<I, T>( | |
523 | items: &I, | |
524 | i: usize, | |
525 | overhead: usize, | |
526 | max_budget: usize, | |
527 | ) -> usize | |
528 | where | |
529 | I: IntoIterator<Item = T> + Clone, | |
530 | T: AsRef<ListItem>, | |
531 | { | |
532 | let mut max_width = 0; | |
533 | let mut first = true; | |
534 | for item in items.clone().into_iter().skip(i) { | |
535 | let item = item.as_ref(); | |
536 | let inner_item_width = item.inner_as_ref().len(); | |
537 | if !first | |
538 | && (item.is_different_group() | |
539 | || item.post_comment.is_none() | |
540 | || inner_item_width + overhead > max_budget) | |
541 | { | |
542 | return max_width; | |
543 | } | |
544 | if max_width < inner_item_width { | |
545 | max_width = inner_item_width; | |
546 | } | |
547 | if item.new_lines { | |
548 | return max_width; | |
549 | } | |
550 | first = false; | |
551 | } | |
552 | max_width | |
553 | } | |
554 | ||
555 | fn post_comment_alignment(item_max_width: Option<usize>, inner_item_len: usize) -> usize { | |
556 | item_max_width.unwrap_or(0).saturating_sub(inner_item_len) | |
557 | } | |
558 | ||
559 | pub(crate) struct ListItems<'a, I, F1, F2, F3> | |
560 | where | |
561 | I: Iterator, | |
562 | { | |
563 | snippet_provider: &'a SnippetProvider, | |
564 | inner: Peekable<I>, | |
565 | get_lo: F1, | |
566 | get_hi: F2, | |
567 | get_item_string: F3, | |
568 | prev_span_end: BytePos, | |
569 | next_span_start: BytePos, | |
570 | terminator: &'a str, | |
571 | separator: &'a str, | |
572 | leave_last: bool, | |
573 | } | |
574 | ||
575 | pub(crate) fn extract_pre_comment(pre_snippet: &str) -> (Option<String>, ListItemCommentStyle) { | |
576 | let trimmed_pre_snippet = pre_snippet.trim(); | |
577 | // Both start and end are checked to support keeping a block comment inline with | |
5e7ed085 | 578 | // the item, even if there are preceding line comments, while still supporting |
f20569fa XL |
579 | // a snippet that starts with a block comment but also contains one or more |
580 | // trailing single line comments. | |
581 | // https://github.com/rust-lang/rustfmt/issues/3025 | |
582 | // https://github.com/rust-lang/rustfmt/pull/3048 | |
583 | // https://github.com/rust-lang/rustfmt/issues/3839 | |
584 | let starts_with_block_comment = trimmed_pre_snippet.starts_with("/*"); | |
585 | let ends_with_block_comment = trimmed_pre_snippet.ends_with("*/"); | |
586 | let starts_with_single_line_comment = trimmed_pre_snippet.starts_with("//"); | |
587 | if ends_with_block_comment { | |
588 | let comment_end = pre_snippet.rfind(|c| c == '/').unwrap(); | |
589 | if pre_snippet[comment_end..].contains('\n') { | |
590 | ( | |
591 | Some(trimmed_pre_snippet.to_owned()), | |
592 | ListItemCommentStyle::DifferentLine, | |
593 | ) | |
594 | } else { | |
595 | ( | |
596 | Some(trimmed_pre_snippet.to_owned()), | |
597 | ListItemCommentStyle::SameLine, | |
598 | ) | |
599 | } | |
600 | } else if starts_with_single_line_comment || starts_with_block_comment { | |
601 | ( | |
602 | Some(trimmed_pre_snippet.to_owned()), | |
603 | ListItemCommentStyle::DifferentLine, | |
604 | ) | |
605 | } else { | |
606 | (None, ListItemCommentStyle::None) | |
607 | } | |
608 | } | |
609 | ||
610 | pub(crate) fn extract_post_comment( | |
611 | post_snippet: &str, | |
612 | comment_end: usize, | |
613 | separator: &str, | |
5e7ed085 | 614 | is_last: bool, |
f20569fa XL |
615 | ) -> Option<String> { |
616 | let white_space: &[_] = &[' ', '\t']; | |
617 | ||
618 | // Cleanup post-comment: strip separators and whitespace. | |
619 | let post_snippet = post_snippet[..comment_end].trim(); | |
5e7ed085 FG |
620 | |
621 | let last_inline_comment_ends_with_separator = if is_last { | |
622 | if let Some(line) = post_snippet.lines().last() { | |
623 | line.ends_with(separator) && line.trim().starts_with("//") | |
624 | } else { | |
625 | false | |
626 | } | |
627 | } else { | |
628 | false | |
629 | }; | |
630 | ||
f20569fa XL |
631 | let post_snippet_trimmed = if post_snippet.starts_with(|c| c == ',' || c == ':') { |
632 | post_snippet[1..].trim_matches(white_space) | |
94222f64 XL |
633 | } else if let Some(stripped) = post_snippet.strip_prefix(separator) { |
634 | stripped.trim_matches(white_space) | |
5e7ed085 FG |
635 | } else if last_inline_comment_ends_with_separator { |
636 | // since we're on the last item it's fine to keep any trailing separators in comments | |
637 | post_snippet.trim_matches(white_space) | |
f20569fa XL |
638 | } |
639 | // not comment or over two lines | |
640 | else if post_snippet.ends_with(',') | |
641 | && (!post_snippet.trim().starts_with("//") || post_snippet.trim().contains('\n')) | |
642 | { | |
643 | post_snippet[..(post_snippet.len() - 1)].trim_matches(white_space) | |
644 | } else { | |
645 | post_snippet | |
646 | }; | |
647 | // FIXME(#3441): post_snippet includes 'const' now | |
648 | // it should not include here | |
649 | let removed_newline_snippet = post_snippet_trimmed.trim(); | |
650 | if !post_snippet_trimmed.is_empty() | |
651 | && (removed_newline_snippet.starts_with("//") || removed_newline_snippet.starts_with("/*")) | |
652 | { | |
653 | Some(post_snippet_trimmed.to_owned()) | |
654 | } else { | |
655 | None | |
656 | } | |
657 | } | |
658 | ||
659 | pub(crate) fn get_comment_end( | |
660 | post_snippet: &str, | |
661 | separator: &str, | |
662 | terminator: &str, | |
663 | is_last: bool, | |
664 | ) -> usize { | |
665 | if is_last { | |
666 | return post_snippet | |
667 | .find_uncommented(terminator) | |
668 | .unwrap_or_else(|| post_snippet.len()); | |
669 | } | |
670 | ||
671 | let mut block_open_index = post_snippet.find("/*"); | |
672 | // check if it really is a block comment (and not `//*` or a nested comment) | |
673 | if let Some(i) = block_open_index { | |
674 | match post_snippet.find('/') { | |
675 | Some(j) if j < i => block_open_index = None, | |
676 | _ if post_snippet[..i].ends_with('/') => block_open_index = None, | |
677 | _ => (), | |
678 | } | |
679 | } | |
680 | let newline_index = post_snippet.find('\n'); | |
681 | if let Some(separator_index) = post_snippet.find_uncommented(separator) { | |
682 | match (block_open_index, newline_index) { | |
683 | // Separator before comment, with the next item on same line. | |
684 | // Comment belongs to next item. | |
685 | (Some(i), None) if i > separator_index => separator_index + 1, | |
686 | // Block-style post-comment before the separator. | |
687 | (Some(i), None) => cmp::max( | |
688 | find_comment_end(&post_snippet[i..]).unwrap() + i, | |
689 | separator_index + 1, | |
690 | ), | |
691 | // Block-style post-comment. Either before or after the separator. | |
692 | (Some(i), Some(j)) if i < j => cmp::max( | |
693 | find_comment_end(&post_snippet[i..]).unwrap() + i, | |
694 | separator_index + 1, | |
695 | ), | |
696 | // Potential *single* line comment. | |
697 | (_, Some(j)) if j > separator_index => j + 1, | |
698 | _ => post_snippet.len(), | |
699 | } | |
700 | } else if let Some(newline_index) = newline_index { | |
701 | // Match arms may not have trailing comma. In any case, for match arms, | |
702 | // we will assume that the post comment belongs to the next arm if they | |
703 | // do not end with trailing comma. | |
704 | newline_index + 1 | |
705 | } else { | |
706 | 0 | |
707 | } | |
708 | } | |
709 | ||
710 | // Account for extra whitespace between items. This is fiddly | |
711 | // because of the way we divide pre- and post- comments. | |
712 | pub(crate) fn has_extra_newline(post_snippet: &str, comment_end: usize) -> bool { | |
713 | if post_snippet.is_empty() || comment_end == 0 { | |
714 | return false; | |
715 | } | |
716 | ||
717 | let len_last = post_snippet[..comment_end] | |
718 | .chars() | |
719 | .last() | |
720 | .unwrap() | |
721 | .len_utf8(); | |
722 | // Everything from the separator to the next item. | |
723 | let test_snippet = &post_snippet[comment_end - len_last..]; | |
724 | let first_newline = test_snippet | |
725 | .find('\n') | |
726 | .unwrap_or_else(|| test_snippet.len()); | |
727 | // From the end of the first line of comments. | |
728 | let test_snippet = &test_snippet[first_newline..]; | |
729 | let first = test_snippet | |
730 | .find(|c: char| !c.is_whitespace()) | |
731 | .unwrap_or_else(|| test_snippet.len()); | |
732 | // From the end of the first line of comments to the next non-whitespace char. | |
733 | let test_snippet = &test_snippet[..first]; | |
734 | ||
735 | // There were multiple line breaks which got trimmed to nothing. | |
736 | count_newlines(test_snippet) > 1 | |
737 | } | |
738 | ||
739 | impl<'a, T, I, F1, F2, F3> Iterator for ListItems<'a, I, F1, F2, F3> | |
740 | where | |
741 | I: Iterator<Item = T>, | |
742 | F1: Fn(&T) -> BytePos, | |
743 | F2: Fn(&T) -> BytePos, | |
744 | F3: Fn(&T) -> Option<String>, | |
745 | { | |
746 | type Item = ListItem; | |
747 | ||
748 | fn next(&mut self) -> Option<Self::Item> { | |
749 | self.inner.next().map(|item| { | |
750 | // Pre-comment | |
751 | let pre_snippet = self | |
752 | .snippet_provider | |
753 | .span_to_snippet(mk_sp(self.prev_span_end, (self.get_lo)(&item))) | |
754 | .unwrap_or(""); | |
755 | let (pre_comment, pre_comment_style) = extract_pre_comment(pre_snippet); | |
756 | ||
757 | // Post-comment | |
758 | let next_start = match self.inner.peek() { | |
759 | Some(next_item) => (self.get_lo)(next_item), | |
760 | None => self.next_span_start, | |
761 | }; | |
762 | let post_snippet = self | |
763 | .snippet_provider | |
764 | .span_to_snippet(mk_sp((self.get_hi)(&item), next_start)) | |
765 | .unwrap_or(""); | |
5e7ed085 FG |
766 | let is_last = self.inner.peek().is_none(); |
767 | let comment_end = | |
768 | get_comment_end(post_snippet, self.separator, self.terminator, is_last); | |
f20569fa | 769 | let new_lines = has_extra_newline(post_snippet, comment_end); |
5e7ed085 FG |
770 | let post_comment = |
771 | extract_post_comment(post_snippet, comment_end, self.separator, is_last); | |
f20569fa XL |
772 | |
773 | self.prev_span_end = (self.get_hi)(&item) + BytePos(comment_end as u32); | |
774 | ||
775 | ListItem { | |
776 | pre_comment, | |
777 | pre_comment_style, | |
778 | item: if self.inner.peek().is_none() && self.leave_last { | |
779 | None | |
780 | } else { | |
781 | (self.get_item_string)(&item) | |
782 | }, | |
783 | post_comment, | |
784 | new_lines, | |
785 | } | |
786 | }) | |
787 | } | |
788 | } | |
789 | ||
790 | #[allow(clippy::too_many_arguments)] | |
791 | // Creates an iterator over a list's items with associated comments. | |
792 | pub(crate) fn itemize_list<'a, T, I, F1, F2, F3>( | |
793 | snippet_provider: &'a SnippetProvider, | |
794 | inner: I, | |
795 | terminator: &'a str, | |
796 | separator: &'a str, | |
797 | get_lo: F1, | |
798 | get_hi: F2, | |
799 | get_item_string: F3, | |
800 | prev_span_end: BytePos, | |
801 | next_span_start: BytePos, | |
802 | leave_last: bool, | |
803 | ) -> ListItems<'a, I, F1, F2, F3> | |
804 | where | |
805 | I: Iterator<Item = T>, | |
806 | F1: Fn(&T) -> BytePos, | |
807 | F2: Fn(&T) -> BytePos, | |
808 | F3: Fn(&T) -> Option<String>, | |
809 | { | |
810 | ListItems { | |
811 | snippet_provider, | |
812 | inner: inner.peekable(), | |
813 | get_lo, | |
814 | get_hi, | |
815 | get_item_string, | |
816 | prev_span_end, | |
817 | next_span_start, | |
818 | terminator, | |
819 | separator, | |
820 | leave_last, | |
821 | } | |
822 | } | |
823 | ||
824 | /// Returns the count and total width of the list items. | |
825 | fn calculate_width<I, T>(items: I) -> (usize, usize) | |
826 | where | |
827 | I: IntoIterator<Item = T>, | |
828 | T: AsRef<ListItem>, | |
829 | { | |
830 | items | |
831 | .into_iter() | |
832 | .map(|item| total_item_width(item.as_ref())) | |
833 | .fold((0, 0), |acc, l| (acc.0 + 1, acc.1 + l)) | |
834 | } | |
835 | ||
836 | pub(crate) fn total_item_width(item: &ListItem) -> usize { | |
837 | comment_len(item.pre_comment.as_ref().map(|x| &(*x)[..])) | |
838 | + comment_len(item.post_comment.as_ref().map(|x| &(*x)[..])) | |
3c0e092e | 839 | + item.item.as_ref().map_or(0, |s| unicode_str_width(s)) |
f20569fa XL |
840 | } |
841 | ||
842 | fn comment_len(comment: Option<&str>) -> usize { | |
843 | match comment { | |
844 | Some(s) => { | |
845 | let text_len = s.trim().len(); | |
846 | if text_len > 0 { | |
847 | // We'll put " /*" before and " */" after inline comments. | |
848 | text_len + 6 | |
849 | } else { | |
850 | text_len | |
851 | } | |
852 | } | |
853 | None => 0, | |
854 | } | |
855 | } | |
856 | ||
857 | // Compute horizontal and vertical shapes for a struct-lit-like thing. | |
858 | pub(crate) fn struct_lit_shape( | |
859 | shape: Shape, | |
860 | context: &RewriteContext<'_>, | |
861 | prefix_width: usize, | |
862 | suffix_width: usize, | |
863 | ) -> Option<(Option<Shape>, Shape)> { | |
864 | let v_shape = match context.config.indent_style() { | |
865 | IndentStyle::Visual => shape | |
866 | .visual_indent(0) | |
867 | .shrink_left(prefix_width)? | |
868 | .sub_width(suffix_width)?, | |
869 | IndentStyle::Block => { | |
870 | let shape = shape.block_indent(context.config.tab_spaces()); | |
871 | Shape { | |
872 | width: context.budget(shape.indent.width()), | |
873 | ..shape | |
874 | } | |
875 | } | |
876 | }; | |
877 | let shape_width = shape.width.checked_sub(prefix_width + suffix_width); | |
878 | if let Some(w) = shape_width { | |
cdc7bbd5 | 879 | let shape_width = cmp::min(w, context.config.struct_lit_width()); |
f20569fa XL |
880 | Some((Some(Shape::legacy(shape_width, shape.indent)), v_shape)) |
881 | } else { | |
882 | Some((None, v_shape)) | |
883 | } | |
884 | } | |
885 | ||
886 | // Compute the tactic for the internals of a struct-lit-like thing. | |
887 | pub(crate) fn struct_lit_tactic( | |
888 | h_shape: Option<Shape>, | |
889 | context: &RewriteContext<'_>, | |
890 | items: &[ListItem], | |
891 | ) -> DefinitiveListTactic { | |
892 | if let Some(h_shape) = h_shape { | |
893 | let prelim_tactic = match (context.config.indent_style(), items.len()) { | |
894 | (IndentStyle::Visual, 1) => ListTactic::HorizontalVertical, | |
895 | _ if context.config.struct_lit_single_line() => ListTactic::HorizontalVertical, | |
896 | _ => ListTactic::Vertical, | |
897 | }; | |
898 | definitive_tactic(items, prelim_tactic, Separator::Comma, h_shape.width) | |
899 | } else { | |
900 | DefinitiveListTactic::Vertical | |
901 | } | |
902 | } | |
903 | ||
904 | // Given a tactic and possible shapes for horizontal and vertical layout, | |
905 | // come up with the actual shape to use. | |
906 | pub(crate) fn shape_for_tactic( | |
907 | tactic: DefinitiveListTactic, | |
908 | h_shape: Option<Shape>, | |
909 | v_shape: Shape, | |
910 | ) -> Shape { | |
911 | match tactic { | |
912 | DefinitiveListTactic::Horizontal => h_shape.unwrap(), | |
913 | _ => v_shape, | |
914 | } | |
915 | } | |
916 | ||
917 | // Create a ListFormatting object for formatting the internals of a | |
918 | // struct-lit-like thing, that is a series of fields. | |
919 | pub(crate) fn struct_lit_formatting<'a>( | |
920 | shape: Shape, | |
921 | tactic: DefinitiveListTactic, | |
922 | context: &'a RewriteContext<'_>, | |
923 | force_no_trailing_comma: bool, | |
924 | ) -> ListFormatting<'a> { | |
925 | let ends_with_newline = context.config.indent_style() != IndentStyle::Visual | |
926 | && tactic == DefinitiveListTactic::Vertical; | |
927 | ListFormatting { | |
928 | tactic, | |
929 | separator: ",", | |
930 | trailing_separator: if force_no_trailing_comma { | |
931 | SeparatorTactic::Never | |
932 | } else { | |
933 | context.config.trailing_comma() | |
934 | }, | |
935 | separator_place: SeparatorPlace::Back, | |
936 | shape, | |
937 | ends_with_newline, | |
938 | preserve_newline: true, | |
939 | nested: false, | |
940 | align_comments: true, | |
941 | config: context.config, | |
942 | } | |
943 | } |