llm 1.3.8

A Rust library unifying multiple LLM backends.
Documentation
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;

use super::wrap::wrap_ranges;

#[derive(Debug, Default, Clone)]
pub struct InputBuffer {
    text: String,
    cursor: usize,
}

impl InputBuffer {
    pub fn text(&self) -> &str {
        &self.text
    }

    pub fn is_empty(&self) -> bool {
        self.text.is_empty()
    }

    pub fn set_text(&mut self, value: String) {
        self.text = value;
        self.cursor = self.cursor.min(self.text.len());
    }

    pub fn take_text(&mut self) -> String {
        self.cursor = 0;
        std::mem::take(&mut self.text)
    }

    pub fn insert_str(&mut self, value: &str) {
        let pos = self.cursor.min(self.text.len());
        self.text.insert_str(pos, value);
        self.cursor = pos + value.len();
    }

    pub fn insert_char(&mut self, ch: char) {
        let pos = self.cursor.min(self.text.len());
        self.text.insert(pos, ch);
        self.cursor = pos + ch.len_utf8();
    }

    pub fn backspace(&mut self) {
        if let Some(prev) = self.prev_grapheme_start() {
            self.text.replace_range(prev..self.cursor, "");
            self.cursor = prev;
        }
    }

    pub fn delete(&mut self) {
        let next = self.next_grapheme_start();
        if let Some(next_idx) = next {
            self.text.replace_range(self.cursor..next_idx, "");
        }
    }

    pub fn move_left(&mut self) {
        if let Some(prev) = self.prev_grapheme_start() {
            self.cursor = prev;
        }
    }

    pub fn move_right(&mut self) {
        if let Some(next) = self.next_grapheme_start() {
            self.cursor = next;
        }
    }

    pub fn move_up(&mut self, width: u16) {
        let (row, col) = self.cursor_position(width);
        if row == 0 {
            return;
        }
        let ranges = wrap_ranges(&self.text, width);
        if let Some(range) = ranges.get(row.saturating_sub(1) as usize) {
            self.cursor = cursor_at_column(&self.text, range.start, range.end, col);
        }
    }

    pub fn move_down(&mut self, width: u16) {
        let (row, col) = self.cursor_position(width);
        let ranges = wrap_ranges(&self.text, width);
        if row as usize + 1 >= ranges.len() {
            return;
        }
        if let Some(range) = ranges.get(row as usize + 1) {
            self.cursor = cursor_at_column(&self.text, range.start, range.end, col);
        }
    }

    pub fn move_home(&mut self) {
        let start = self.text[..self.cursor]
            .rfind('\n')
            .map(|idx| idx + 1)
            .unwrap_or(0);
        self.cursor = start;
    }

    pub fn move_end(&mut self) {
        let end = self.text[self.cursor..]
            .find('\n')
            .map(|idx| self.cursor + idx)
            .unwrap_or(self.text.len());
        self.cursor = end;
    }

    pub fn newline(&mut self) {
        self.insert_char('\n');
    }

    pub fn wrapped_lines(&self, width: u16) -> Vec<String> {
        wrap_ranges(&self.text, width)
            .into_iter()
            .map(|range| self.text[range].to_string())
            .collect()
    }

    pub fn cursor_position(&self, width: u16) -> (u16, u16) {
        let ranges = wrap_ranges(&self.text, width);
        for (row, range) in ranges.iter().enumerate() {
            if self.cursor >= range.start && self.cursor <= range.end {
                let slice = &self.text[range.start..self.cursor];
                let col = slice.width() as u16;
                return (row as u16, col);
            }
        }
        (0, 0)
    }

    fn prev_grapheme_start(&self) -> Option<usize> {
        if self.cursor == 0 {
            return None;
        }
        self.text[..self.cursor]
            .grapheme_indices(true)
            .next_back()
            .map(|(idx, _)| idx)
    }

    fn next_grapheme_start(&self) -> Option<usize> {
        if self.cursor >= self.text.len() {
            return None;
        }
        let remaining = &self.text[self.cursor..];
        remaining
            .grapheme_indices(true)
            .nth(1)
            .map(|(idx, _)| self.cursor + idx)
            .or(Some(self.text.len()))
    }
}

fn cursor_at_column(text: &str, start: usize, end: usize, target_col: u16) -> usize {
    let mut width = 0usize;
    let slice = &text[start..end];
    for (idx, grapheme) in slice.grapheme_indices(true) {
        width += grapheme.width();
        if width as u16 >= target_col {
            return start + idx + grapheme.len();
        }
    }
    end
}