textpos 0.3.3

A small library to track human-readable text positions
Documentation
use std::{
    fmt::{Display, Debug},
    str::FromStr
};

use crate::TextPosition;

/// Describes a position between two characters.
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct InsertPosition {
    line: usize,
    col: usize,
}

impl Default for InsertPosition {
    fn default() -> Self {
        Self { line: 1, col: 0 }
    }
}

impl InsertPosition {
    /// Crate a new `InsertPosition` from raw parts
    pub fn new(line: usize, col: usize) -> Self {
        assert_ne!(line, 0);
        InsertPosition { line, col }
    }

    /// The line this position is on. 1 is the first line.
    ///
    /// This is always equals to the number of `inc_line()` calls + 1 made on
    /// this position.
    pub fn line(&self) -> usize { self.line }
    /// The column to the left of the position. 0 is the first insert position,
    /// when there is no column left of the position.
    ///
    /// This is always equals to the number of `inc_col()` calls made on this
    /// position after the last `inc_line()` call.
    pub fn col(&self) -> usize { self.col }

    /// Move the position forward by one non-newline character.
    pub fn inc_col(&mut self) {
        self.col += 1;
    }
    /// Move the position forward by one newline character.
    pub fn inc_line(&mut self) {
        self.line += 1;
        self.col = 0;
    }
    /// Move the position forward by one character.
    /// If the character `c` is a newline, it behaves like [`inc_line()`],
    /// otherwhise it behaves like [`inc_col()`].
    pub fn inc(&mut self, c: char) {
        if c == '\n' {
            self.inc_line();
        } else {
            self.inc_col();
        }
    }

    pub(crate) fn text_pos_left(&self) -> TextPosition {
        TextPosition::new(self.line(), self.col())
    }
    pub(crate) fn text_pos_right(&self) -> TextPosition {
        TextPosition::new(self.line(), self.col()+1)
    }
}

impl Display for InsertPosition {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}:{}", self.line(), self.col())
    }
}
impl Debug for InsertPosition {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self)
    }
}

impl FromStr for InsertPosition {
    type Err = ();

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let (line_str, col_str) = s.trim().split_once(':').ok_or(())?;
        let line: usize = line_str.parse().map_err(|_| ())?;
        let col: usize = col_str.parse().map_err(|_| ())?;

        // Validate
        if line == 0 { return Err(()); }

        Ok(InsertPosition::new(line, col))
    }
}


#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn initializes_correctly() {
        let default = InsertPosition::default();
        assert_eq!(default.line(), 1);
        assert_eq!(default.col(), 0);
    }

    #[test]
    fn increment_methods() {
        let mut pos = InsertPosition::default();

        pos.inc_col();
        pos.inc_col();
        pos.inc_col();

        assert_eq!(pos.line(), 1);
        assert_eq!(pos.col(), 3);

        pos.inc_col();
        pos.inc_line();
        pos.inc_col();

        assert_eq!(pos.line(), 2);
        assert_eq!(pos.col(), 1);

        pos.inc_col();
        pos.inc_line();
        pos.inc_line();

        assert_eq!(pos.line(), 4);
        assert_eq!(pos.col(), 0);
    }

    #[test]
    fn display_print() {
        let mut pos = InsertPosition::default();
        pos.inc_col();
        pos.inc_line();

        let text_repr = format!("{}", pos);
        assert_eq!(text_repr, "2:0");
    }

    #[test]
    fn debug_print() {
        let mut pos = InsertPosition::default();
        pos.inc_col();
        pos.inc_line();

        let text_repr = format!("{:?}", pos);
        assert_eq!(text_repr, "2:0");
    }

    #[test]
    fn ordering_and_eq() {
        let mut pos1 = InsertPosition::default();
        pos1.inc_col();
        pos1.inc_line();
        pos1.inc_col();

        let mut pos2 = pos1.clone();
        pos2.inc_line();

        let pos3 = pos2.clone();

        assert!(pos2 > pos1);
        assert_eq!(pos2, pos3);
    }

    #[test]
    fn parsing() {
        assert_eq!("4:7".parse(), Ok(InsertPosition::new(4, 7)));
        assert_eq!("1:0".parse(), Ok(InsertPosition::new(1, 0)));
        assert_eq!("1:-9".parse::<InsertPosition>(), Err(()));
        assert_eq!("0:6".parse::<InsertPosition>(), Err(()));
    }
}