noline 0.5.1

A no_std line editor
Documentation
fn distance_from_window(start: isize, end: isize, point: isize) -> isize {
    if point < start {
        point - start
    } else if point > end {
        point - end
    } else {
        0
    }
}

#[cfg_attr(test, derive(Debug))]
#[derive(Copy, Clone, PartialEq, Eq)]
pub struct Cursor {
    pub row: usize,
    pub column: usize,
}

impl Cursor {
    pub fn new(row: usize, column: usize) -> Self {
        Self { row, column }
    }
}

#[cfg_attr(test, derive(Debug))]
#[derive(Copy, Clone, PartialEq, Eq)]
pub struct Position {
    pub row: usize,
    pub column: usize,
}

impl Position {
    pub fn new(row: usize, column: usize) -> Self {
        Self { row, column }
    }
}

#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
pub struct Terminal {
    rows: usize,
    columns: usize,
    cursor: Cursor,
    row_offset: isize,
}

impl Default for Terminal {
    fn default() -> Self {
        Self::new(24, 80, Cursor::new(0, 0))
    }
}

impl Terminal {
    pub fn new(rows: usize, columns: usize, cursor: Cursor) -> Self {
        let row_offset = -(cursor.row as isize);

        Self {
            rows,
            columns,
            cursor,
            row_offset,
        }
    }

    pub fn resize(&mut self, rows: usize, columns: usize) {
        self.rows = rows;
        self.columns = columns;
    }

    pub fn reset(&mut self, cursor: Cursor) {
        self.cursor = cursor;
        self.row_offset = -(cursor.row as isize);
    }

    pub fn get_cursor(&self) -> Cursor {
        self.cursor
    }

    pub fn get_position(&self) -> Position {
        self.cursor_to_position(self.cursor)
    }

    pub fn scrolling_needed(&self, position: Position) -> isize {
        distance_from_window(
            self.row_offset,
            self.row_offset + self.rows as isize - 1,
            position.row as isize,
        )
    }

    pub fn scroll_to_top(&mut self) -> isize {
        let rows = self.row_offset;
        self.row_offset = 0;

        rows
    }

    pub fn scroll(&mut self, rows: isize) {
        self.row_offset += rows;
    }

    pub fn move_cursor(&mut self, position: Position) -> isize {
        let rows = self.scrolling_needed(position);
        self.scroll(rows);

        #[cfg(test)]
        dbg!(rows, position);

        self.cursor = self
            .position_to_cursor(position)
            .unwrap_or_else(|| unreachable!());

        rows
    }

    pub fn move_cursor_to_start_of_line(&mut self) {
        self.cursor.column = 0;
    }

    pub fn position_to_cursor(&self, position: Position) -> Option<Cursor> {
        let row = position.row as isize - self.row_offset;

        if row >= 0 && row < self.rows as isize {
            Some(Cursor::new(row as usize, position.column))
        } else {
            None
        }
    }

    pub fn cursor_to_position(&self, position: Cursor) -> Position {
        #[cfg(test)]
        dbg!(self.row_offset);

        Position::new(
            (position.row as isize + self.row_offset) as usize,
            position.column,
        )
    }

    pub fn offset_from_position(&self, position: Position) -> isize {
        position.row as isize * self.columns as isize + position.column as isize
    }

    pub fn current_offset(&self) -> isize {
        let position = self.cursor_to_position(self.cursor);
        self.offset_from_position(position)
    }

    fn position_from_offset(&self, offset: isize) -> Position {
        let row = offset.div_euclid(self.columns as isize);
        let column = offset.rem_euclid(self.columns as isize);
        Position::new(row as usize, column as usize)
    }

    pub fn relative_position(&self, steps: isize) -> Position {
        let offset = self.offset_from_position(self.cursor_to_position(self.cursor));

        self.position_from_offset(offset + steps)
    }

    pub fn columns_remaining(&self) -> usize {
        self.columns - self.cursor.column
    }

    #[cfg(test)]
    pub fn get_size(&self) -> (usize, usize) {
        (self.rows, self.columns)
    }
}

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

    #[test]
    fn test_distance_from_window() {
        assert_eq!(distance_from_window(4, 8, 2), -2);
        assert_eq!(distance_from_window(4, 8, 4), 0);
        assert_eq!(distance_from_window(4, 8, 8), 0);
        assert_eq!(distance_from_window(4, 8, 10), 2);

        assert_eq!(distance_from_window(-3, 8, 2), 0);
        assert_eq!(distance_from_window(-3, 8, -5), -2);
        assert_eq!(distance_from_window(-3, 8, 9), 1);
    }

    #[test]
    fn position_from_top() {
        let term = Terminal::new(4, 10, Cursor::new(0, 0));

        assert_eq!(
            term.cursor_to_position(term.get_cursor()),
            Position::new(0, 0)
        );

        assert_eq!(
            term.cursor_to_position(Cursor::new(3, 9)),
            Position::new(3, 9)
        );

        assert_eq!(
            term.cursor_to_position(Cursor::new(4, 9)),
            Position::new(4, 9)
        );

        assert_eq!(
            term.position_to_cursor(Position::new(3, 9)),
            Some(Cursor::new(3, 9))
        );

        assert_eq!(term.position_to_cursor(Position::new(4, 9)), None);
    }

    #[test]
    fn position_from_second_line() {
        let term = Terminal::new(4, 10, Cursor::new(1, 0));

        assert_eq!(
            term.cursor_to_position(term.get_cursor()),
            Position::new(0, 0)
        );

        assert_eq!(
            term.cursor_to_position(Cursor::new(3, 9)),
            Position::new(2, 9)
        );

        assert_eq!(
            term.position_to_cursor(Position::new(2, 9)),
            Some(Cursor::new(3, 9))
        );
    }

    #[test]
    fn position_scroll() {
        let mut term = Terminal::new(4, 10, Cursor::new(0, 0));

        assert_eq!(term.move_cursor(Position::new(7, 0)), 4);

        assert_eq!(
            term.cursor_to_position(term.get_cursor()),
            Position::new(7, 0)
        );

        assert_eq!(
            term.cursor_to_position(Cursor::new(3, 9)),
            Position::new(7, 9)
        );

        assert_eq!(
            term.cursor_to_position(Cursor::new(0, 0)),
            Position::new(4, 0)
        );

        assert_eq!(term.position_to_cursor(Position::new(2, 9)), None);
    }

    #[test]
    fn position_scroll_offset() {
        let mut term = Terminal::new(4, 10, Cursor::new(3, 9));

        let position = term.relative_position(1);

        assert_eq!(position, Position::new(1, 0));
        assert_eq!(term.position_to_cursor(position), None);

        assert_eq!(term.move_cursor(Position::new(1, 0)), 1);
    }

    #[test]
    fn move_cursor() {
        let mut term = Terminal::new(4, 10, Cursor::new(0, 0));

        let pos = Position::new(3, 9);
        assert_eq!(term.scrolling_needed(pos), 0);

        assert_eq!(term.move_cursor(pos), 0);

        let pos = Position::new(4, 0);
        assert_eq!(term.scrolling_needed(pos), 1);

        assert_eq!(term.move_cursor(pos), 1);

        assert_eq!(term.get_cursor(), Cursor::new(3, 0));
        assert_eq!(term.get_position(), Position::new(4, 0));
        assert_eq!(term.current_offset(), 40);

        let pos = Position::new(0, 0);
        assert_eq!(term.scrolling_needed(pos), -1);

        assert_eq!(term.move_cursor(pos), -1);

        assert_eq!(term.get_cursor(), Cursor::new(0, 0));
        assert_eq!(term.get_position(), Position::new(0, 0));
    }

    #[test]
    fn offset() {
        let term = Terminal::new(4, 10, Cursor::new(1, 0));

        assert_eq!(term.get_cursor(), Cursor::new(1, 0));
        assert_eq!(term.get_position(), Position::new(0, 0));
        assert_eq!(term.current_offset(), 0);
    }
}