textpos 0.3.3

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

use crate::{InsertPosition, TextPosition};

/// Defines a range in a text.
///
/// A range is defined by two [`InsertPosition`]s, `start` and `end`. All the
/// text between the two positions is included in the range.
#[derive(Default, Clone, PartialEq, Eq)]
pub struct TextRange {
    start: InsertPosition,
    end: InsertPosition,
}

impl TextRange {
    /// Create a new text range containing everything between `start` and `end`.
    ///
    /// They may point to the same location (in which case the range is empty),
    /// but `end` may not be before `start`.
    pub fn new(start: InsertPosition, end: InsertPosition) -> Self {
        assert!(start <= end);
        TextRange { start, end }
    }
    /// Create a new empty range at `pos`.
    pub fn new_empty(pos: InsertPosition) -> Self {
        Self::new(pos.clone(), pos)
    }

    /// Append a non-newline character to the range.
    pub fn inc_col(&mut self) {
        self.end.inc_col();
    }
    /// Append a newline character to the range.
    pub fn inc_line(&mut self) {
        self.end.inc_line();
    }
    /// Append one character to the range.
    /// 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();
        }
    }

    /// Merge `other` into `self`.
    ///
    /// `self.end()` must line up (ie. be equal to) `other.start()`. This means
    /// there may be no characters between the two ranges.
    pub fn merge(&mut self, other: TextRange) {
        assert!(self.end() == other.start());

        self.end = other.end;
    }

    /// Create a new empty range starting at the end of `self`
    pub fn begin_next_range(&self) -> Self {
        TextRange::new_empty(self.end().clone())
    }

    /// Get the start insert position
    pub fn start(&self) -> &InsertPosition { &self.start }
    /// Get the end insert position
    pub fn end(&self) -> &InsertPosition { &self.end }
}

impl Display for TextRange {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        if self.start() == self.end() {
            return write!(f, "empty after {}", self.start());
        }

        let start = self.start().text_pos_right();
        let end = self.end().text_pos_left();

        if start == end {
            write!(f, "{}", start)
        } else {
            write!(f, "{} - {}", start, end)
        }
    }
}
impl Debug for TextRange {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self)
    }
}

impl FromStr for TextRange {
    type Err = ();

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.split_once('-') {
            None => {
                // Only one position -> one textposition
                let pos: TextPosition = s.parse()?;
                Ok(TextRange::new(pos.insert_pos_left(), pos.insert_pos_right()))
            }
            Some((start, end)) => {
                // Two positions -> start and end textpositions
                let start = start.trim().parse::<TextPosition>()?.insert_pos_left();
                let end = end.trim().parse::<TextPosition>()?.insert_pos_right();

                // Validate
                if end < start { return Err(()); }

                Ok(TextRange::new(start, end))
            }
        }
    }
}


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

    #[test]
    #[should_panic]
    fn disallow_reverse_base_next() {
        let mut pos1 = InsertPosition::default();
        let pos2 = InsertPosition::default();

        pos1.inc_col();

        let _ = TextRange::new(pos1, pos2);
    }


    #[test]
    fn allow_creaton_empty_range() {
        let pos1 = InsertPosition::default();

        let empty = TextRange::new_empty(pos1);
        assert_eq!(empty, TextRange::default());
    }

    #[test]
    fn merge() {
        let mut ran1 = TextRange::default();
        ran1.inc_col();

        let mut pos1 = InsertPosition::default();
        pos1.inc_col();
        let mut pos2 = pos1.clone();
        pos2.inc_col();
        let ran2 = TextRange::new(pos1, pos2.clone());

        let ran3 = TextRange::new_empty(pos2);

        ran1.merge(ran2);
        ran1.merge(ran3);
    }

    #[test]
    #[should_panic]
    fn merge_should_fail() {
        let mut ran1 = TextRange::default();

        let mut pos1 = InsertPosition::default();
        pos1.inc_col();
        pos1.inc_col();
        let ran2 = TextRange::new_empty(pos1);

        ran1.merge(ran2);
    }

    #[test]
    fn begin_next_range() {
        let mut ran1 = TextRange::default();
        ran1.inc_line();
        ran1.inc_col();
        ran1.inc_col();
        let ran2 = ran1.begin_next_range();

        assert_eq!(ran2, TextRange::new_empty(InsertPosition::new(2, 2)));
    }

    #[test]
    fn display_print() {
        let mut ran1 = TextRange::default();
        assert_eq!(ran1.to_string(), "empty after 1:0");

        ran1.inc_line();
        ran1.inc_line();
        ran1.inc_col();
        ran1.inc_col();
        ran1.inc_col();
        assert_eq!(ran1.to_string(), "1:1 - 3:3");

        // Test zeroth-column char
        ran1.inc_line();
        assert_eq!(ran1.to_string(), "1:1 - 4:0");
    }

    #[test]
    fn debug_print() {
        let mut ran1 = TextRange::default();
        assert_eq!(format!("{:?}", ran1), "empty after 1:0");

        ran1.inc_line();
        ran1.inc_line();
        ran1.inc_col();
        ran1.inc_col();
        ran1.inc_col();
        assert_eq!(format!("{:?}", ran1), "1:1 - 3:3");

        // Test zeroth-column char
        ran1.inc_line();
        assert_eq!(format!("{:?}", ran1), "1:1 - 4:0");
    }

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