ted 0.1.0

Core text editor functionality.
Documentation
//! Functions that compute a new char index.

use ropey::Rope;
use std::cmp::min;

pub fn right(text: &Rope, char_idx: usize) -> usize {
    let len_chars = text.len_chars();
    min(
        if len_chars > 0 { len_chars } else { 0 },
        char_idx + 1
    )
}

pub fn left(char_idx: usize) -> usize {
    if char_idx > 0 {
        char_idx - 1
    } else {
        0
    }
}

pub fn down(text: &Rope, char_idx: usize) -> usize {
    vertical(text, char_idx, 1)
}

pub fn up(text: &Rope, char_idx: usize) -> usize {
    vertical(text, char_idx, -1)
}

fn vertical(text: &Rope, char_idx: usize, lines: isize) -> usize {
    // > If char_idx is one-past-the-end, then one-past-the-end line index is returned.
    // Circumvent this behavior by clipping to the last valid line index.
    let line_idx = min(text.len_lines() - 1, text.char_to_line(char_idx));
    if lines < 0 && line_idx > 0 || lines > 0 && line_idx < text.len_lines() - 1 {
        let x = char_idx - text.line_to_char(line_idx);
        let next_line_idx = if lines > 0 {
            line_idx + lines as usize
        } else {
            line_idx - lines.abs() as usize
        };
        let line_len = text.line(next_line_idx).len_chars();

        text.line_to_char(next_line_idx) +
        if x <= line_len {
            x
        } else if line_len > 0 {
            line_len - 1
        } else {
            0
        }
    } else {
        char_idx
    }
}

pub fn line_start(text: &Rope, char_idx: usize) -> usize {
    let line_idx = text.char_to_line(char_idx);
    text.line_to_char(line_idx)
}

pub fn line_end(text: &Rope, char_idx: usize) -> usize {
    let line_idx = text.char_to_line(char_idx);
    text.line_to_char(line_idx) + if line_idx < text.len_lines() {
        let line_len = text.line(line_idx).len_chars();
        if line_len > 0 {
            line_len - 1
        } else {
            0
        }
    } else {
        0
    }
}

#[cfg(test)]
mod tests {
    use ropey::Rope;

    const TEXT: &str = concat!(
        "1st\n",
        "2nd line\n",
        "3rd line"
    );

    #[test]
    fn right() {{
        let text = Rope::from_str(TEXT);
        let len_chars = text.len_chars();
        assert_eq!(1,             super::right(&text, 0));
        assert_eq!(len_chars - 1, super::right(&text, len_chars - 2));
        assert_eq!(len_chars,     super::right(&text, len_chars - 1));
        assert_eq!(len_chars,     super::right(&text, len_chars));
    } {
        let text = Rope::from_str("");
        assert_eq!(0, super::right(&text, 0));
    }}

    #[test]
    fn left() {
        assert_eq!(0, super::left(0));
        assert_eq!(0, super::left(1));
        assert_eq!(1, super::left(2));
    }

    #[test]
    fn down() {{
        let text = Rope::from_str(TEXT);
        assert_eq!(text.line_to_char(1),     super::down(&text, text.line_to_char(0)));
        assert_eq!(text.line_to_char(2),     super::down(&text, text.line_to_char(1)));
        assert_eq!(text.line_to_char(2),     super::down(&text, text.line_to_char(2)));
        assert_eq!(text.line_to_char(2) + 3, super::down(&text, text.line_to_char(1) + 3));
        assert_eq!(text.len_chars(),         super::down(&text, text.line_to_char(2) - 1));
    } {
        let text = Rope::from_str("");
        assert_eq!(0, super::down(&text, 0));
    }}

    #[test]
    fn up() {{
        let text = Rope::from_str(TEXT);
        assert_eq!(text.line_to_char(1), super::up(&text, text.line_to_char(2)));
        assert_eq!(text.line_to_char(0), super::up(&text, text.line_to_char(1)));
        assert_eq!(text.line_to_char(0), super::up(&text, text.line_to_char(0)));
        assert_eq!(text.line_to_char(0) + 3, super::up(&text, text.line_to_char(1) + 3));
        assert_eq!(text.line_to_char(1) - 1, super::up(&text, text.line_to_char(2) - 2));
    } {
        let text = Rope::from_str("");
        assert_eq!(0, super::up(&text, 0));
    }}

    #[test]
    fn line_start() {{
        let text = Rope::from_str(TEXT);
        assert_eq!(text.line_to_char(1), super::line_start(&text, 12));
        assert_eq!(text.line_to_char(1), super::line_start(&text, text.line_to_char(1)));
    } {
        let text = Rope::from_str("");
        assert_eq!(0, super::line_start(&text, 0));
    }}

    #[test]
    fn line_end() {{
        let text = Rope::from_str(TEXT);
        assert_eq!(text.line_to_char(2) - 1, super::line_end(&text, 12));
        assert_eq!(text.line_to_char(2) - 1, super::line_end(&text, text.line_to_char(2) - 1));
    } {
        let text = Rope::from_str("");
        assert_eq!(0, super::line_end(&text, 0));
    }}
}