alma 0.1.0

A Bevy-native modal text editor with Vim-style navigation.
Documentation
//! Vertical line-wise motion helpers.

use super::clamp_to_cursor_position;

/// Direction for line-wise movement.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(super) enum VerticalDirection {
    /// Move toward the previous line.
    Up,
    /// Move toward the next line.
    Down,
}

/// Returns the start/end byte indices and character column for the line containing `index`.
fn line_bounds_and_column(text: &str, index: usize) -> (usize, usize, usize) {
    let index = clamp_to_cursor_position(text, index);
    let line_start = text[..index]
        .rfind('\n')
        .map_or(0, |newline_index| newline_index + '\n'.len_utf8());
    let line_end = text[index..]
        .find('\n')
        .map_or(text.len(), |newline_offset| index + newline_offset);
    let column = text[line_start..index].chars().count();

    (line_start, line_end, column)
}

/// Returns the visible character column for `index`.
pub(super) fn character_column(text: &str, index: usize) -> usize {
    let (_line_start, _line_end, column) = line_bounds_and_column(text, index);
    column
}

/// Returns the visible cursor byte index for `column` in the provided line bounds.
fn byte_index_for_column(text: &str, line_start: usize, line_end: usize, column: usize) -> usize {
    let line_character_count = text[line_start..line_end].chars().count();

    if line_character_count == 0 {
        return line_start;
    }

    let column = column.min(line_character_count - 1);

    text[line_start..line_end]
        .char_indices()
        .nth(column)
        .map_or(line_end, |(offset, _character)| line_start + offset)
}

/// Moves vertically while keeping the current character column where possible.
pub(super) fn vertical_move(text: &str, index: usize, direction: VerticalDirection) -> usize {
    let column = character_column(text, index);

    vertical_move_to_column(text, index, direction, column)
}

/// Moves vertically toward `direction`, preserving `column` where possible.
pub(super) fn vertical_move_to_column(
    text: &str,
    index: usize,
    direction: VerticalDirection,
    column: usize,
) -> usize {
    let (line_start, line_end, _current_column) = line_bounds_and_column(text, index);

    match direction {
        VerticalDirection::Up => {
            if line_start == 0 {
                return clamp_to_cursor_position(text, index);
            }

            let previous_line_end = line_start - '\n'.len_utf8();
            let previous_line_start = text[..previous_line_end]
                .rfind('\n')
                .map_or(0, |newline_index| newline_index + '\n'.len_utf8());

            byte_index_for_column(text, previous_line_start, previous_line_end, column)
        }
        VerticalDirection::Down => {
            if line_end == text.len() {
                return clamp_to_cursor_position(text, index);
            }

            let next_line_start = line_end + '\n'.len_utf8();
            let next_line_end = text[next_line_start..]
                .find('\n')
                .map_or(text.len(), |newline_offset| {
                    next_line_start + newline_offset
                });

            byte_index_for_column(text, next_line_start, next_line_end, column)
        }
    }
}