pub mod history;
#[cfg(test)]
mod tests;
use std::convert::Infallible;
use std::str::FromStr;
use unicode_segmentation::UnicodeSegmentation;
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct PromptInput {
text: String,
cursor: usize,
}
impl From<String> for PromptInput {
fn from(s: String) -> Self {
Self::from_text(&s)
}
}
impl From<&str> for PromptInput {
fn from(s: &str) -> Self {
Self::from_text(s)
}
}
impl FromStr for PromptInput {
type Err = Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self::from_text(s))
}
}
impl PromptInput {
pub fn new() -> Self {
Self::default()
}
fn from_text(s: &str) -> Self {
let cursor = s.graphemes(true).count();
Self { text: s.to_string(), cursor }
}
pub fn as_str(&self) -> &str {
&self.text
}
pub fn text(&self) -> String {
self.text.clone()
}
pub fn cursor(&self) -> usize {
self.cursor
}
pub fn len_graphemes(&self) -> usize {
self.text.graphemes(true).count()
}
pub fn graphemes(&self) -> unicode_segmentation::Graphemes<'_> {
self.text.graphemes(true)
}
pub fn is_empty(&self) -> bool {
self.text.is_empty()
}
pub fn clear(&mut self) {
self.text.clear();
self.cursor = 0;
}
pub fn set_text(&mut self, s: &str) {
self.text = s.to_string();
self.cursor = self.len_graphemes();
}
pub fn cursor_left(&mut self) {
self.cursor = self.cursor.saturating_sub(1);
}
pub fn cursor_right(&mut self) {
if self.cursor < self.len_graphemes() {
self.cursor += 1;
}
}
pub fn cursor_to_start(&mut self) {
self.cursor = 0;
}
pub fn cursor_to_end(&mut self) {
self.cursor = self.len_graphemes();
}
pub fn cursor_up(&mut self) -> bool {
let graphemes: Vec<&str> = self.graphemes().collect();
let Some((line_start, column)) = line_start_and_column(&graphemes, self.cursor) else {
return false;
};
if line_start == 0 {
return false;
}
let prev_end = line_start.saturating_sub(1);
let prev_start = graphemes[..prev_end]
.iter()
.rposition(|g| g.contains('\n'))
.map_or(0, |idx| idx + 1);
let prev_len = prev_end.saturating_sub(prev_start);
self.cursor = prev_start + column.min(prev_len);
true
}
pub fn cursor_down(&mut self) -> bool {
let graphemes: Vec<&str> = self.graphemes().collect();
let Some((line_start, column)) = line_start_and_column(&graphemes, self.cursor) else {
return false;
};
let current_end = graphemes[line_start..]
.iter()
.position(|g| g.contains('\n'))
.map(|offset| line_start + offset);
let Some(current_end) = current_end else {
return false;
};
let next_start = current_end + 1;
let next_end = graphemes[next_start..]
.iter()
.position(|g| g.contains('\n'))
.map_or(graphemes.len(), |offset| next_start + offset);
let next_len = next_end.saturating_sub(next_start);
self.cursor = next_start + column.min(next_len);
true
}
pub fn cursor_word_left(&mut self) {
let graphemes: Vec<&str> = self.graphemes().collect();
if self.cursor == 0 {
return;
}
self.cursor = prev_word_boundary(&graphemes, self.cursor);
}
pub fn cursor_word_right(&mut self) {
let graphemes: Vec<&str> = self.graphemes().collect();
let len = graphemes.len();
if self.cursor >= len {
return;
}
self.cursor = next_word_boundary(&graphemes, self.cursor);
}
pub fn insert_char(&mut self, ch: char) {
let byte_idx = self.byte_offset_of(self.cursor);
self.text.insert(byte_idx, ch);
self.cursor += 1;
}
pub fn insert_str(&mut self, s: &str) {
let count = s.graphemes(true).count();
if count == 0 {
return;
}
let byte_idx = self.byte_offset_of(self.cursor);
self.text.insert_str(byte_idx, s);
self.cursor += count;
}
pub fn replace_range(&mut self, start: usize, end: usize, replacement: &str) {
let len = self.len_graphemes();
let start = start.min(len);
let end = end.min(len).max(start);
let start_byte = self.byte_offset_of(start);
let end_byte = self.byte_offset_of(end);
self.text.replace_range(start_byte..end_byte, replacement);
self.cursor = start + replacement.graphemes(true).count();
}
pub fn backspace(&mut self) -> bool {
if self.cursor == 0 {
return false;
}
let graphemes: Vec<&str> = self.graphemes().collect();
let prev = graphemes[self.cursor - 1];
let byte_idx = self.byte_offset_of(self.cursor - 1);
self.text.replace_range(byte_idx..byte_idx + prev.len(), "");
self.cursor -= 1;
true
}
pub fn delete_forward(&mut self) -> bool {
let len = self.len_graphemes();
if self.cursor >= len {
return false;
}
let graphemes: Vec<&str> = self.graphemes().collect();
let cur = graphemes[self.cursor];
let byte_idx = self.byte_offset_of(self.cursor);
self.text.replace_range(byte_idx..byte_idx + cur.len(), "");
true
}
pub fn kill_to_end_of_line(&mut self) -> String {
let graphemes: Vec<&str> = self.graphemes().collect();
let line_end = graphemes[self.cursor..]
.iter()
.position(|g| g.contains('\n'))
.map(|offset| self.cursor + offset)
.unwrap_or_else(|| self.len_graphemes());
let byte_idx = self.byte_offset_of(self.cursor);
let end_byte = self.byte_offset_of(line_end);
let killed = self.text[byte_idx..end_byte].to_string();
self.text.replace_range(byte_idx..end_byte, "");
killed
}
pub fn kill_to_start_of_line(&mut self) -> String {
let graphemes: Vec<&str> = self.graphemes().collect();
let line_start = graphemes[..self.cursor]
.iter()
.rposition(|g| g.contains('\n'))
.map_or(0, |idx| idx + 1);
let start_byte = self.byte_offset_of(line_start);
let end_byte = self.byte_offset_of(self.cursor);
let killed = self.text[start_byte..end_byte].to_string();
self.text.replace_range(start_byte..end_byte, "");
self.cursor = line_start;
killed
}
pub fn kill_word_left(&mut self) -> String {
let graphemes: Vec<&str> = self.graphemes().collect();
if self.cursor == 0 {
return String::new();
}
let target = prev_word_boundary(&graphemes, self.cursor);
let start_byte = self.byte_offset_of(target);
let end_byte = self.byte_offset_of(self.cursor);
let killed = self.text[start_byte..end_byte].to_string();
self.text.replace_range(start_byte..end_byte, "");
self.cursor = target;
killed
}
pub fn kill_word_right(&mut self) -> String {
let graphemes: Vec<&str> = self.graphemes().collect();
let len = graphemes.len();
if self.cursor >= len {
return String::new();
}
let target = next_word_boundary(&graphemes, self.cursor);
let start_byte = self.byte_offset_of(self.cursor);
let end_byte = self.byte_offset_of(target);
let killed = self.text[start_byte..end_byte].to_string();
self.text.replace_range(start_byte..end_byte, "");
killed
}
pub fn transpose_chars(&mut self) -> bool {
let graphemes: Vec<&str> = self.graphemes().collect();
if graphemes.len() < 2 {
return false;
}
let (a, b) = if self.cursor >= graphemes.len() {
(graphemes.len() - 2, graphemes.len() - 1)
} else if self.cursor == 0 {
(0, 1)
} else {
(self.cursor - 1, self.cursor)
};
let byte_a = self.byte_offset_of(a);
let byte_b = self.byte_offset_of(b);
let combined = format!("{}{}", graphemes[b], graphemes[a]);
self.text.replace_range(byte_a..byte_b + graphemes[b].len(), &combined);
self.cursor = b + 1;
true
}
pub fn yank(&mut self, text: &str) {
self.insert_str(text);
}
fn byte_offset_of(&self, grapheme_index: usize) -> usize {
self.text
.grapheme_indices(true)
.nth(grapheme_index)
.map(|(byte_idx, _)| byte_idx)
.unwrap_or_else(|| self.text.len())
}
pub fn text_before_cursor(&self) -> &str {
let byte_idx = self.byte_offset_of(self.cursor);
&self.text[..byte_idx]
}
}
fn line_start_and_column(graphemes: &[&str], cursor: usize) -> Option<(usize, usize)> {
if cursor > graphemes.len() {
return None;
}
let line_start = graphemes[..cursor]
.iter()
.rposition(|g| g.contains('\n'))
.map_or(0, |idx| idx + 1);
Some((line_start, cursor.saturating_sub(line_start)))
}
fn prev_word_boundary(graphemes: &[&str], cursor: usize) -> usize {
if cursor == 0 {
return 0;
}
let combined: String = graphemes[..cursor].concat();
let segments: Vec<(usize, &str)> = combined
.split_word_bound_indices()
.map(|(byte_off, word)| {
let g_off = combined[..byte_off].graphemes(true).count();
(g_off, word)
})
.collect();
for &(start, word) in segments.iter().rev() {
if start < cursor && !word.trim().is_empty() {
return start;
}
}
0
}
fn next_word_boundary(graphemes: &[&str], cursor: usize) -> usize {
let len = graphemes.len();
if cursor >= len {
return len;
}
let combined: String = graphemes[cursor..].concat();
let segments: Vec<(usize, &str)> = combined
.split_word_bound_indices()
.map(|(byte_off, word)| {
let g_off = combined[..byte_off].graphemes(true).count();
(g_off, word)
})
.collect();
for &(start, word) in segments.iter() {
if start > 0 && !word.trim().is_empty() {
return cursor + start;
}
}
len
}