// Copyright 2015 The Rust Project Developers. See the COPYRIGHT // file at the top-level directory of this distribution and at // http://rust-lang.org/COPYRIGHT. // // Licensed under the Apache License, Version 2.0 or the MIT license // , at your // option. This file may not be copied, modified, or distributed // except according to those terms. use std::cmp; use syntax::codemap::{self, CodeMap, BytePos}; use utils::{round_up_to_power_of_two, make_indent}; use comment::{FindUncommented, rewrite_comment, find_comment_end}; #[derive(Eq, PartialEq, Debug, Copy, Clone)] pub enum ListTactic { // One item per row. Vertical, // All items on one row. Horizontal, // Try Horizontal layout, if that fails then vertical HorizontalVertical, // Pack as many items as possible per row over (possibly) many rows. Mixed, } #[derive(Eq, PartialEq, Debug, Copy, Clone)] pub enum SeparatorTactic { Always, Never, Vertical, } impl_enum_decodable!(SeparatorTactic, Always, Never, Vertical); // TODO having some helpful ctors for ListFormatting would be nice. pub struct ListFormatting<'a> { pub tactic: ListTactic, pub separator: &'a str, pub trailing_separator: SeparatorTactic, pub indent: usize, // Available width if we layout horizontally. pub h_width: usize, // Available width if we layout vertically pub v_width: usize, // Non-expressions, e.g. items, will have a new line at the end of the list. // Important for comment styles. pub ends_with_newline: bool, } pub struct ListItem { pub pre_comment: Option, // Item should include attributes and doc comments pub item: String, pub post_comment: Option, } impl ListItem { pub fn is_multiline(&self) -> bool { self.item.contains('\n') || self.pre_comment.is_some() || self.post_comment.as_ref().map(|s| s.contains('\n')).unwrap_or(false) } pub fn has_line_pre_comment(&self) -> bool { self.pre_comment.as_ref().map_or(false, |comment| comment.starts_with("//")) } pub fn from_str>(s: S) -> ListItem { ListItem { pre_comment: None, item: s.into(), post_comment: None } } } // Format a list of commented items into a string. // FIXME: this has grown into a monstrosity // TODO: add unit tests pub fn write_list<'b>(items: &[ListItem], formatting: &ListFormatting<'b>) -> String { if items.len() == 0 { return String::new(); } let mut tactic = formatting.tactic; // Conservatively overestimates because of the changing separator tactic. let sep_count = if formatting.trailing_separator != SeparatorTactic::Never { items.len() } else { items.len() - 1 }; let sep_len = formatting.separator.len(); let total_sep_len = (sep_len + 1) * sep_count; let total_width = calculate_width(items); let fits_single = total_width + total_sep_len <= formatting.h_width; // Check if we need to fallback from horizontal listing, if possible. if tactic == ListTactic::HorizontalVertical { debug!("write_list: total_width: {}, total_sep_len: {}, h_width: {}", total_width, total_sep_len, formatting.h_width); tactic = if fits_single && !items.iter().any(ListItem::is_multiline) { ListTactic::Horizontal } else { ListTactic::Vertical }; } // Check if we can fit everything on a single line in mixed mode. // The horizontal tactic does not break after v_width columns. if tactic == ListTactic::Mixed && fits_single { tactic = ListTactic::Horizontal; } // Switch to vertical mode if we find non-block comments. if items.iter().any(ListItem::has_line_pre_comment) { tactic = ListTactic::Vertical; } // Now that we know how we will layout, we can decide for sure if there // will be a trailing separator. let trailing_separator = needs_trailing_separator(formatting.trailing_separator, tactic); // Create a buffer for the result. // TODO could use a StringBuffer or rope for this let alloc_width = if tactic == ListTactic::Horizontal { total_width + total_sep_len } else { total_width + items.len() * (formatting.indent + 1) }; let mut result = String::with_capacity(round_up_to_power_of_two(alloc_width)); let mut line_len = 0; let indent_str = &make_indent(formatting.indent); for (i, item) in items.iter().enumerate() { let first = i == 0; let last = i == items.len() - 1; let separate = !last || trailing_separator; let item_sep_len = if separate { sep_len } else { 0 }; let item_width = item.item.len() + item_sep_len; match tactic { ListTactic::Horizontal if !first => { result.push(' '); } ListTactic::Vertical if !first => { result.push('\n'); result.push_str(indent_str); } ListTactic::Mixed => { let total_width = total_item_width(item) + item_sep_len; if line_len > 0 && line_len + total_width > formatting.v_width { result.push('\n'); result.push_str(indent_str); line_len = 0; } if line_len > 0 { result.push(' '); line_len += 1; } line_len += total_width; } _ => {} } // Pre-comments if let Some(ref comment) = item.pre_comment { result.push_str(&rewrite_comment(comment, // Block style in non-vertical mode tactic != ListTactic::Vertical, // Width restriction is only // relevant in vertical mode. formatting.v_width, formatting.indent)); if tactic == ListTactic::Vertical { result.push('\n'); result.push_str(indent_str); } else { result.push(' '); } } result.push_str(&item.item); // Post-comments if tactic != ListTactic::Vertical && item.post_comment.is_some() { let formatted_comment = rewrite_comment(item.post_comment.as_ref().unwrap(), true, formatting.v_width, 0); result.push(' '); result.push_str(&formatted_comment); } if separate { result.push_str(formatting.separator); } if tactic == ListTactic::Vertical && item.post_comment.is_some() { // 1 = space between item and comment. let width = formatting.v_width.checked_sub(item_width + 1).unwrap_or(1); let offset = formatting.indent + item_width + 1; let comment = item.post_comment.as_ref().unwrap(); // Use block-style only for the last item or multiline comments. let block_style = formatting.ends_with_newline && last || comment.trim().contains('\n') || comment.trim().len() > width; let formatted_comment = rewrite_comment(comment, block_style, width, offset); result.push(' '); result.push_str(&formatted_comment); } } result } // Turns a list into a vector of items with associated comments. // TODO: we probably do not want to take a terminator any more. Instead, we // should demand a proper span end. pub fn itemize_list(codemap: &CodeMap, prefix: Vec, it: I, separator: &str, terminator: &str, get_lo: F1, get_hi: F2, get_item_string: F3, mut prev_span_end: BytePos, next_span_start: BytePos) -> Vec where I: Iterator, F1: Fn(&T) -> BytePos, F2: Fn(&T) -> BytePos, F3: Fn(&T) -> String { let mut result = prefix; result.reserve(it.size_hint().0); let mut new_it = it.peekable(); let white_space: &[_] = &[' ', '\t']; while let Some(item) = new_it.next() { // Pre-comment let pre_snippet = codemap.span_to_snippet(codemap::mk_sp(prev_span_end, get_lo(&item))) .unwrap(); let pre_snippet = pre_snippet.trim(); let pre_comment = if pre_snippet.len() > 0 { Some(pre_snippet.to_owned()) } else { None }; // Post-comment let next_start = match new_it.peek() { Some(ref next_item) => get_lo(next_item), None => next_span_start }; let post_snippet = codemap.span_to_snippet(codemap::mk_sp(get_hi(&item), next_start)) .unwrap(); let comment_end = match new_it.peek() { Some(..) => { let block_open_index = post_snippet.find("/*"); let newline_index = post_snippet.find('\n'); let separator_index = post_snippet.find_uncommented(separator).unwrap(); match (block_open_index, newline_index) { // Separator before comment, with the next item on same line. // Comment belongs to next item. (Some(i), None) if i > separator_index => { separator_index + separator.len() } // Block-style post-comment before the separator. (Some(i), None) => { cmp::max(find_comment_end(&post_snippet[i..]).unwrap() + i, separator_index + separator.len()) } // Block-style post-comment. Either before or after the separator. (Some(i), Some(j)) if i < j => { cmp::max(find_comment_end(&post_snippet[i..]).unwrap() + i, separator_index + separator.len()) } // Potential *single* line comment. (_, Some(j)) => { j + 1 } _ => post_snippet.len() } }, None => { post_snippet.find_uncommented(terminator) .unwrap_or(post_snippet.len()) } }; // Cleanup post-comment: strip separators and whitespace. prev_span_end = get_hi(&item) + BytePos(comment_end as u32); let mut post_snippet = post_snippet[..comment_end].trim(); if post_snippet.starts_with(separator) { post_snippet = post_snippet[separator.len()..] .trim_matches(white_space); } else if post_snippet.ends_with(separator) { post_snippet = post_snippet[..post_snippet.len()-separator.len()] .trim_matches(white_space); } result.push(ListItem { pre_comment: pre_comment, item: get_item_string(&item), post_comment: if post_snippet.len() > 0 { Some(post_snippet.to_owned()) } else { None } }); } result } fn needs_trailing_separator(separator_tactic: SeparatorTactic, list_tactic: ListTactic) -> bool { match separator_tactic { SeparatorTactic::Always => true, SeparatorTactic::Vertical => list_tactic == ListTactic::Vertical, SeparatorTactic::Never => false, } } fn calculate_width(items: &[ListItem]) -> usize { items.iter().map(total_item_width).fold(0, |a, l| a + l) } fn total_item_width(item: &ListItem) -> usize { comment_len(&item.pre_comment) + comment_len(&item.post_comment) + item.item.len() } fn comment_len(comment: &Option) -> usize { match comment { &Some(ref s) => { let text_len = s.trim().len(); if text_len > 0 { // We'll put " /*" before and " */" after inline comments. text_len + 6 } else { text_len } }, &None => 0 } }