1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
use super::Span;
use std::fmt;
use std::fs::File;
use std::path::{Path, PathBuf};
use std::io::{self, Read, Write};

pub struct FileText {
    path: PathBuf,
    input_str: String,
    newlines: Vec<usize>,
}

impl FileText {
    pub fn from_path(path: PathBuf) -> io::Result<FileText> {
        let mut input_str = String::new();
        let mut f = try!(File::open(&path));
        try!(f.read_to_string(&mut input_str));
        Ok(FileText::new(path, input_str))
    }

    fn new(path: PathBuf, input_str: String) -> FileText {
        let newline_indices: Vec<usize> = {
            let input_indices =
                input_str.as_bytes().iter()
                                    .enumerate()
                                    .filter(|&(_, &b)| b == ('\n' as u8))
                                    .map(|(i, _)| i + 1); // index of first char in the line
            Some(0).into_iter()
                   .chain(input_indices)
                   .collect()
        };

        FileText { path: path, input_str: input_str, newlines: newline_indices }
    }

    pub fn path(&self) -> &Path {
        &self.path
    }

    pub fn text(&self) -> &String {
        &self.input_str
    }

    pub fn line_col(&self, pos: usize) -> (usize, usize) {
        let num_lines = self.newlines.len();
        let line =
            (0..num_lines)
            .filter(|&i| self.newlines[i] > pos)
            .map(|i| i-1)
            .next()
            .unwrap_or(num_lines - 1);

        // offset of the first character in `line`
        let line_offset = self.newlines[line];

        // find the column; use `saturating_sub` in case `pos` is the
        // newline itself, which we'll call column 0
        let col = pos - line_offset;

        (line, col)
    }

    fn line_text(&self, line_num: usize) -> &str {
        let start_offset = self.newlines[line_num];
        if line_num == self.newlines.len() - 1 {
            &self.input_str[start_offset..]
        } else {
            let end_offset = self.newlines[line_num + 1];
            &self.input_str[start_offset..end_offset-1]
        }
    }

    pub fn highlight(&self, span: Span, out: &mut Write) -> io::Result<()> {
        let (start_line, start_col) = self.line_col(span.0);
        let (end_line, end_col) = self.line_col(span.1);

        // (*) use `saturating_sub` since the start line could be the newline
        // itself, in which case we'll call it column zero

        // span is within one line:
        if start_line == end_line {
            let text = self.line_text(start_line);
            try!(writeln!(out, "  {}", text));

            if end_col - start_col <= 1 {
                try!(writeln!(out, "  {}^", Repeat(' ', start_col)));
            } else {
                let width = end_col - start_col;
                try!(writeln!(out, "  {}~{}~",
                              Repeat(' ', start_col),
                              Repeat('~', width.saturating_sub(2))));
            }
        } else {
            // span is across many lines, find the maximal width of any of those
            let line_strs: Vec<_> = (start_line..end_line+1).map(|i| self.line_text(i)).collect();
            let max_len = line_strs.iter().map(|l| l.len()).max().unwrap();
            try!(writeln!(out, "  {}{}~+",
                          Repeat(' ', start_col),
                          Repeat('~', max_len - start_col)));
            for line in &line_strs[..line_strs.len()-1] {
                try!(writeln!(out, "| {0:<1$} |", line, max_len));
            }
            try!(writeln!(out, "| {}", line_strs[line_strs.len()-1]));
            try!(writeln!(out, "+~{}", Repeat('~', end_col)));
        }

        Ok(())
    }
}

struct Repeat(char, usize);

impl fmt::Display for Repeat {
    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
        for _ in 0..self.1 {
            try!(write!(fmt, "{}", self.0));
        }
        Ok(())
    }
}