innards 0.3.0

Inline terminal tools for Rust symbol navigation, editing, and paging
Documentation
use std::ops::Range;

use ropey::Rope;

use super::editor::Editor;

pub(super) fn line_selection_range(
    app: &Editor,
    line: usize,
    active_region: Option<&Range<usize>>,
) -> Option<Range<usize>> {
    let active_region = active_region?;
    let line_start = line_start_char(&app.buffer, line);
    let line_end = line_start + line_len_chars(&app.buffer, line);
    let start = active_region.start.max(line_start);
    let end = active_region.end.min(line_end);
    if start < end {
        Some(start - line_start..end - line_start)
    } else {
        None
    }
}

pub(super) fn buffer_line_count(buffer: &Rope) -> usize {
    buffer.len_lines().max(1)
}

pub(super) fn line_start_char(buffer: &Rope, line: usize) -> usize {
    buffer.line_to_char(line.min(buffer_line_count(buffer).saturating_sub(1)))
}

pub(super) fn line_len_chars(buffer: &Rope, line: usize) -> usize {
    let line = buffer.line(line.min(buffer_line_count(buffer).saturating_sub(1)));
    let mut len = line.len_chars();
    if len > 0 && line.char(len - 1) == '\n' {
        len -= 1;
        if len > 0 && line.char(len - 1) == '\r' {
            len -= 1;
        }
    }
    len
}

pub(super) fn line_text(buffer: &Rope, line: usize) -> String {
    let line = line.min(buffer_line_count(buffer).saturating_sub(1));
    let len = line_len_chars(buffer, line);
    buffer.line(line).slice(..len).to_string()
}

pub(super) fn resize_anchor_row(
    previous_top: u16,
    previous_height: u16,
    new_height: u16,
    terminal_rows: u16,
) -> u16 {
    let anchor = if new_height < previous_height {
        previous_top.saturating_add(previous_height - new_height)
    } else {
        previous_top
    };
    anchor.min(terminal_rows.saturating_sub(1))
}

pub(super) fn common_indent_len(lines: &[String]) -> usize {
    lines
        .iter()
        .filter(|line| !line.trim().is_empty())
        .map(|line| line.chars().take_while(|ch| ch.is_whitespace()).count())
        .min()
        .unwrap_or(0)
}

pub(super) fn wrap_words(words: &[String], indent: &str, column: usize) -> String {
    let indent_len = char_len(indent);
    let target = column.max(indent_len + 1);
    let mut lines = Vec::new();
    let mut current = indent.to_string();
    let mut current_len = indent_len;

    for word in words {
        let word_len = char_len(word);
        let needs_space = current_len > indent_len;
        let next_len = current_len + usize::from(needs_space) + word_len;

        if needs_space && next_len > target {
            lines.push(current);
            current = indent.to_string();
            current.push_str(word);
            current_len = indent_len + word_len;
        } else {
            if needs_space {
                current.push(' ');
                current_len += 1;
            }
            current.push_str(word);
            current_len += word_len;
        }
    }

    lines.push(current);
    lines.join("\n")
}

pub(super) fn char_len(text: &str) -> usize {
    text.chars().count()
}

pub(super) fn byte_index(text: &str, char_idx: usize) -> usize {
    text.char_indices()
        .nth(char_idx)
        .map(|(idx, _)| idx)
        .unwrap_or(text.len())
}

pub(super) fn find_in_line_forward(line: &str, query: &str, start_col: usize) -> Option<usize> {
    let start = byte_index(line, start_col);
    line.get(start..)?
        .find(query)
        .map(|idx| start_col + char_len(&line[start..start + idx]))
}

pub(super) fn find_in_line_reverse(line: &str, query: &str, end_col: usize) -> Option<usize> {
    let end = byte_index(line, end_col);
    line.get(..end)?
        .rfind(query)
        .map(|idx| char_len(&line[..idx]))
}