idet-core 0.1.0

Shared text-editing library for a Micro-like terminal editor and a gedit-like egui GUI
Documentation
//! Text insertion and indentation edits, all character-index based.

use crate::completion::char_to_byte;

const INDENT: &str = "    ";

/// Inserts `insertion` at character index `cursor`, returning the new text and
/// the cursor placed after the inserted text.
#[must_use]
pub fn insert(text: &str, cursor: usize, insertion: &str) -> (String, usize) {
    let byte = char_to_byte(text, cursor);
    let mut result = text.to_owned();
    result.insert_str(byte, insertion);
    (result, cursor + insertion.chars().count())
}

/// Inserts a newline at `cursor` followed by the leading whitespace of the
/// current line, reproducing that line's indentation.
#[must_use]
pub fn newline_indent(text: &str, cursor: usize) -> (String, usize) {
    let byte = char_to_byte(text, cursor);
    let line_start = text[..byte].rfind('\n').map_or(0, |index| index + 1);
    let indent: String = text[line_start..]
        .chars()
        .take_while(|c| *c == ' ' || *c == '\t')
        .collect();
    let mut insertion = String::with_capacity(indent.len() + 1);
    insertion.push('\n');
    insertion.push_str(&indent);
    insert(text, cursor, &insertion)
}

/// Prepends four spaces to the current line, shifting the cursor accordingly.
#[must_use]
pub fn indent_line(text: &str, cursor: usize) -> (String, usize) {
    let start = line_start_char(text, cursor);
    let (result, _) = insert(text, start, INDENT);
    (result, cursor + INDENT.chars().count())
}

/// Removes up to four leading spaces from the current line, clamping the cursor
/// to the line start when it sat inside the removed indentation.
#[must_use]
pub fn dedent_line(text: &str, cursor: usize) -> (String, usize) {
    let start = line_start_char(text, cursor);
    let start_byte = char_to_byte(text, start);
    let removable = text[start_byte..]
        .chars()
        .take(INDENT.len())
        .take_while(|c| *c == ' ')
        .count();
    if removable == 0 {
        return (text.to_owned(), cursor);
    }
    let end_byte = char_to_byte(text, start + removable);
    let mut result = text.to_owned();
    result.replace_range(start_byte..end_byte, "");
    let new_cursor = if cursor >= start + removable {
        cursor - removable
    } else {
        start
    };
    (result, new_cursor)
}

/// Returns the character index of the start of the line containing `cursor`.
#[must_use]
pub fn line_start_char(text: &str, cursor: usize) -> usize {
    let byte = char_to_byte(text, cursor);
    let start_byte = text[..byte].rfind('\n').map_or(0, |index| index + 1);
    text[..start_byte].chars().count()
}

/// Returns the character index of the end of the line containing `cursor`.
#[must_use]
pub fn line_end_char(text: &str, cursor: usize) -> usize {
    let byte = char_to_byte(text, cursor);
    let end_byte = text[byte..]
        .find('\n')
        .map_or(text.len(), |index| byte + index);
    text[..end_byte].chars().count()
}