rust/src/lists.rs

189 lines
6 KiB
Rust
Raw Normal View History

// 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 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
2015-04-21 11:01:19 +02:00
use utils::make_indent;
2015-05-25 09:11:53 +02:00
use rustc_serialize::{Decodable, Decoder};
#[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,
}
2015-05-25 09:11:53 +02:00
// TODO could use a macro for all these Decodable impls.
impl Decodable for SeparatorTactic {
fn decode<D: Decoder>(d: &mut D) -> Result<Self, D::Error> {
let s = try!(d.read_str());
match &*s {
"Always" => Ok(SeparatorTactic::Always),
"Never" => Ok(SeparatorTactic::Never),
"Vertical" => Ok(SeparatorTactic::Vertical),
_ => Err(d.error("Bad variant")),
}
}
}
2015-04-23 07:04:07 +02:00
// 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,
}
// Format a list of strings into a string.
2015-04-21 12:50:43 +02:00
// Precondition: all strings in items are trimmed.
pub fn write_list<'b>(items: &[(String, String)], 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
};
2015-04-21 12:50:43 +02:00
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.
2015-04-23 08:02:55 +02:00
if tactic == ListTactic::HorizontalVertical {
2015-04-23 08:30:12 +02:00
debug!("write_list: total_width: {}, total_sep_len: {}, h_width: {}",
total_width, total_sep_len, formatting.h_width);
tactic = if fits_single {
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;
}
// Now that we know how we will layout, we can decide for sure if there
// will be a trailing separator.
2015-04-21 12:50:43 +02:00
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 {
2015-04-21 12:50:43 +02:00
total_width + total_sep_len
} else {
total_width + items.len() * (formatting.indent + 1)
};
let mut result = String::with_capacity(alloc_width);
let mut line_len = 0;
let indent_str = &make_indent(formatting.indent);
2015-04-21 12:50:43 +02:00
for (i, &(ref item, ref comment)) in items.iter().enumerate() {
let first = i == 0;
let separate = i != items.len() - 1 || trailing_separator;
match tactic {
ListTactic::Horizontal if !first => {
result.push(' ');
}
ListTactic::Vertical if !first => {
result.push('\n');
result.push_str(indent_str);
}
ListTactic::Mixed => {
let mut item_width = item.len();
if separate {
item_width += sep_len;
}
2015-04-21 12:50:43 +02:00
if line_len > 0 && line_len + item_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 += item_width;
}
_ => {}
}
result.push_str(item);
2015-04-23 08:02:55 +02:00
2015-04-21 12:50:43 +02:00
if tactic != ListTactic::Vertical && comment.len() > 0 {
if !comment.starts_with('\n') {
result.push(' ');
}
2015-04-21 12:50:43 +02:00
result.push_str(comment);
}
if separate {
result.push_str(formatting.separator);
}
2015-04-21 12:50:43 +02:00
if tactic == ListTactic::Vertical && comment.len() > 0 {
if !comment.starts_with('\n') {
result.push(' ');
}
2015-04-21 12:50:43 +02:00
result.push_str(comment);
}
}
result
}
2015-04-21 12:50:43 +02:00
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:&[(String, String)]) -> usize {
let missed_width = items.iter().map(|&(_, ref s)| {
let text_len = s.trim().len();
if text_len > 0 {
// We'll put a space before any comment.
text_len + 1
} else {
text_len
}
}).fold(0, |a, l| a + l);
let item_width = items.iter().map(|&(ref s, _)| s.len()).fold(0, |a, l| a + l);
missed_width + item_width
}