diff --git a/Cargo.lock b/Cargo.lock index 162e9160f85..2d0495bc6cb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5,7 +5,9 @@ dependencies = [ "diff 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", "env_logger 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", "getopts 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)", + "itertools 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", + "multimap 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "regex 0.1.63 (registry+https://github.com/rust-lang/crates.io-index)", "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", "strings 0.0.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -47,6 +49,11 @@ name = "getopts" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "itertools" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "kernel32-sys" version = "0.2.1" @@ -79,6 +86,11 @@ name = "mempool" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "multimap" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "regex" version = "0.1.63" diff --git a/Cargo.toml b/Cargo.toml index 6fe6bc05d56..ca4ec71a4a2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,3 +25,5 @@ syntex_syntax = "0.32" log = "0.3" env_logger = "0.3" getopts = "0.2" +itertools = "0.4" +multimap = "0.3" diff --git a/src/config.rs b/src/config.rs index e4d0e4517c2..45bd9a6f7c5 100644 --- a/src/config.rs +++ b/src/config.rs @@ -10,6 +10,7 @@ extern crate toml; +use file_lines::FileLines; use lists::{SeparatorTactic, ListTactic}; use std::io::Write; @@ -200,6 +201,12 @@ impl ConfigType for String { } } +impl ConfigType for FileLines { + fn doc_hint() -> String { + String::from("") + } +} + pub struct ConfigHelpItem { option_name: &'static str, doc_string: &'static str, @@ -327,6 +334,8 @@ macro_rules! create_config { create_config! { verbose: bool, false, "Use verbose output"; skip_children: bool, false, "Don't reformat out of line modules"; + file_lines: FileLines, FileLines::all(), + "Lines to format"; max_width: usize, 100, "Maximum width of each line"; ideal_width: usize, 80, "Ideal width of each line"; tab_spaces: usize, 4, "Number of spaces per tab"; diff --git a/src/file_lines.rs b/src/file_lines.rs new file mode 100644 index 00000000000..e437202360d --- /dev/null +++ b/src/file_lines.rs @@ -0,0 +1,221 @@ +//! This module contains types and functions to support formatting specific line ranges. +use std::{cmp, iter, str}; + +use itertools::Itertools; +use multimap::MultiMap; +use rustc_serialize::{self, json}; + +use codemap::LineRange; + +/// A range that is inclusive of both ends. +#[derive(Clone, Copy, Debug, Eq, PartialEq, RustcDecodable)] +struct Range { + pub lo: usize, + pub hi: usize, +} + +impl<'a> From<&'a LineRange> for Range { + fn from(range: &'a LineRange) -> Range { + Range::new(range.lo, range.hi) + } +} + +impl Range { + fn new(lo: usize, hi: usize) -> Range { + Range { lo: lo, hi: hi } + } + + fn is_empty(self) -> bool { + self.lo > self.hi + } + + fn contains(self, other: Range) -> bool { + if other.is_empty() { + true + } else { + !self.is_empty() && self.lo <= other.lo && self.hi >= other.hi + } + } + + fn intersects(self, other: Range) -> bool { + if self.is_empty() || other.is_empty() { + false + } else { + (self.lo <= other.hi && other.hi <= self.hi) || + (other.lo <= self.hi && self.hi <= other.hi) + } + } + + fn adjacent_to(self, other: Range) -> bool { + if self.is_empty() || other.is_empty() { + false + } else { + self.hi + 1 == other.lo || other.hi + 1 == self.lo + } + } + + /// Returns a new `Range` with lines from `self` and `other` if they were adjacent or + /// intersect; returns `None` otherwise. + fn merge(self, other: Range) -> Option { + if self.adjacent_to(other) || self.intersects(other) { + Some(Range::new(cmp::min(self.lo, other.lo), cmp::max(self.hi, other.hi))) + } else { + None + } + } +} + +/// A set of lines in files. +/// +/// It is represented as a multimap keyed on file names, with values a collection of +/// non-overlapping ranges sorted by their start point. An inner `None` is interpreted to mean all +/// lines in all files. +#[derive(Clone, Debug, Default)] +pub struct FileLines(Option>); + +/// Normalizes the ranges so that the invariants for `FileLines` hold: ranges are non-overlapping, +/// and ordered by their start point. +fn normalize_ranges(map: &mut MultiMap) { + for (_, ranges) in map.iter_all_mut() { + ranges.sort_by_key(|x| x.lo); + let merged = ranges.drain(..).coalesce(|x, y| x.merge(y).ok_or((x, y))).collect(); + *ranges = merged; + } +} + +impl FileLines { + /// Creates a `FileLines` that contains all lines in all files. + pub fn all() -> FileLines { + FileLines(None) + } + + /// Creates a `FileLines` from a `MultiMap`, ensuring that the invariants hold. + fn from_multimap(map: MultiMap) -> FileLines { + let mut map = map; + normalize_ranges(&mut map); + FileLines(Some(map)) + } + + /// Returns an iterator over the files contained in `self`. + pub fn files(&self) -> Files { + Files(self.0.as_ref().map(MultiMap::keys)) + } + + /// Returns true if `range` is fully contained in `self`. + pub fn contains(&self, range: &LineRange) -> bool { + let map = match self.0 { + // `None` means "all lines in all files". + None => return true, + Some(ref map) => map, + }; + + match map.get_vec(range.file_name()) { + None => false, + Some(ranges) => ranges.iter().any(|r| r.contains(Range::from(range))), + } + } + + /// Returns true if any lines in `range` are in `self`. + pub fn intersects(&self, range: &LineRange) -> bool { + let map = match self.0 { + // `None` means "all lines in all files". + None => return true, + Some(ref map) => map, + }; + + match map.get_vec(range.file_name()) { + None => false, + Some(ranges) => ranges.iter().any(|r| r.intersects(Range::from(range))), + } + } +} + +/// FileLines files iterator. +pub struct Files<'a>(Option<::std::collections::hash_map::Keys<'a, String, Vec>>); + +impl<'a> iter::Iterator for Files<'a> { + type Item = &'a String; + + fn next(&mut self) -> Option<&'a String> { + self.0.as_mut().and_then(Iterator::next) + } +} + +// This impl is needed for `Config::override_value` to work for use in tests. +impl str::FromStr for FileLines { + type Err = String; + + fn from_str(s: &str) -> Result { + let v: Vec = try!(json::decode(s).map_err(|e| e.to_string())); + let m = v.into_iter().map(JsonSpan::into_tuple).collect(); + Ok(FileLines::from_multimap(m)) + } +} + +// For JSON decoding. +#[derive(Clone, Debug, RustcDecodable)] +struct JsonSpan { + file: String, + range: (usize, usize), +} + +impl JsonSpan { + // To allow `collect()`ing into a `MultiMap`. + fn into_tuple(self) -> (String, Range) { + let (lo, hi) = self.range; + (self.file, Range::new(lo, hi)) + } +} + +// This impl is needed for inclusion in the `Config` struct. We don't have a toml representation +// for `FileLines`, so it will just panic instead. +impl rustc_serialize::Decodable for FileLines { + fn decode(_: &mut D) -> Result { + unimplemented!(); + } +} + +#[cfg(test)] +mod test { + use super::Range; + + #[test] + fn test_range_intersects() { + assert!(Range::new(1, 2).intersects(Range::new(1, 1))); + assert!(Range::new(1, 2).intersects(Range::new(2, 2))); + assert!(!Range::new(1, 2).intersects(Range::new(0, 0))); + assert!(!Range::new(1, 2).intersects(Range::new(3, 10))); + assert!(!Range::new(1, 3).intersects(Range::new(5, 5))); + } + + #[test] + fn test_range_adjacent_to() { + assert!(!Range::new(1, 2).adjacent_to(Range::new(1, 1))); + assert!(!Range::new(1, 2).adjacent_to(Range::new(2, 2))); + assert!(Range::new(1, 2).adjacent_to(Range::new(0, 0))); + assert!(Range::new(1, 2).adjacent_to(Range::new(3, 10))); + assert!(!Range::new(1, 3).adjacent_to(Range::new(5, 5))); + } + + #[test] + fn test_range_contains() { + assert!(Range::new(1, 2).contains(Range::new(1, 1))); + assert!(Range::new(1, 2).contains(Range::new(2, 2))); + assert!(!Range::new(1, 2).contains(Range::new(0, 0))); + assert!(!Range::new(1, 2).contains(Range::new(3, 10))); + } + + #[test] + fn test_range_merge() { + assert_eq!(None, Range::new(1, 3).merge(Range::new(5, 5))); + assert_eq!(None, Range::new(4, 7).merge(Range::new(0, 1))); + assert_eq!(Some(Range::new(3, 7)), + Range::new(3, 5).merge(Range::new(4, 7))); + assert_eq!(Some(Range::new(3, 7)), + Range::new(3, 5).merge(Range::new(5, 7))); + assert_eq!(Some(Range::new(3, 7)), + Range::new(3, 5).merge(Range::new(6, 7))); + assert_eq!(Some(Range::new(3, 7)), + Range::new(3, 7).merge(Range::new(4, 5))); + } +} diff --git a/src/lib.rs b/src/lib.rs index 12bfc6ef2da..322eed374de 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24,6 +24,8 @@ extern crate unicode_segmentation; extern crate regex; extern crate diff; extern crate term; +extern crate itertools; +extern crate multimap; use syntax::ast; use syntax::codemap::{mk_sp, CodeMap, Span}; @@ -53,6 +55,7 @@ mod utils; pub mod config; pub mod codemap; pub mod filemap; +pub mod file_lines; pub mod visitor; mod checkstyle; mod items;