224 lines
5.8 KiB
Rust
224 lines
5.8 KiB
Rust
use std::collections::VecDeque;
|
|
|
|
/// Text input buffer with history and cursor management.
|
|
#[derive(Debug, Clone)]
|
|
pub struct InputBuffer {
|
|
buffer: String,
|
|
cursor: usize,
|
|
history: VecDeque<String>,
|
|
history_index: Option<usize>,
|
|
max_history: usize,
|
|
pub multiline: bool,
|
|
tab_width: u8,
|
|
}
|
|
|
|
impl InputBuffer {
|
|
/// Create a new input buffer
|
|
pub fn new(max_history: usize, multiline: bool, tab_width: u8) -> Self {
|
|
Self {
|
|
buffer: String::new(),
|
|
cursor: 0,
|
|
history: VecDeque::with_capacity(max_history.max(1)),
|
|
history_index: None,
|
|
max_history: max_history.max(1),
|
|
multiline,
|
|
tab_width: tab_width.max(1),
|
|
}
|
|
}
|
|
|
|
/// Get current text
|
|
pub fn text(&self) -> &str {
|
|
&self.buffer
|
|
}
|
|
|
|
/// Current cursor position
|
|
pub fn cursor(&self) -> usize {
|
|
self.cursor
|
|
}
|
|
|
|
/// Replace buffer contents
|
|
pub fn set_text(&mut self, text: impl Into<String>) {
|
|
self.buffer = text.into();
|
|
self.cursor = self.buffer.len();
|
|
self.history_index = None;
|
|
}
|
|
|
|
/// Clear buffer and reset cursor
|
|
pub fn clear(&mut self) {
|
|
self.buffer.clear();
|
|
self.cursor = 0;
|
|
self.history_index = None;
|
|
}
|
|
|
|
/// Insert a character at the cursor position
|
|
pub fn insert_char(&mut self, ch: char) {
|
|
if ch == '\t' {
|
|
self.insert_tab();
|
|
return;
|
|
}
|
|
|
|
self.buffer.insert(self.cursor, ch);
|
|
self.cursor += ch.len_utf8();
|
|
}
|
|
|
|
/// Insert text at cursor
|
|
pub fn insert_text(&mut self, text: &str) {
|
|
self.buffer.insert_str(self.cursor, text);
|
|
self.cursor += text.len();
|
|
}
|
|
|
|
/// Insert spaces representing a tab
|
|
pub fn insert_tab(&mut self) {
|
|
let spaces = " ".repeat(self.tab_width as usize);
|
|
self.insert_text(&spaces);
|
|
}
|
|
|
|
/// Remove character before cursor
|
|
pub fn backspace(&mut self) {
|
|
if self.cursor == 0 {
|
|
return;
|
|
}
|
|
|
|
let prev_index = prev_char_boundary(&self.buffer, self.cursor);
|
|
self.buffer.drain(prev_index..self.cursor);
|
|
self.cursor = prev_index;
|
|
}
|
|
|
|
/// Remove character at cursor
|
|
pub fn delete(&mut self) {
|
|
if self.cursor >= self.buffer.len() {
|
|
return;
|
|
}
|
|
|
|
let next_index = next_char_boundary(&self.buffer, self.cursor);
|
|
self.buffer.drain(self.cursor..next_index);
|
|
}
|
|
|
|
/// Move cursor left by one grapheme
|
|
pub fn move_left(&mut self) {
|
|
if self.cursor == 0 {
|
|
return;
|
|
}
|
|
self.cursor = prev_char_boundary(&self.buffer, self.cursor);
|
|
}
|
|
|
|
/// Move cursor right by one grapheme
|
|
pub fn move_right(&mut self) {
|
|
if self.cursor >= self.buffer.len() {
|
|
return;
|
|
}
|
|
self.cursor = next_char_boundary(&self.buffer, self.cursor);
|
|
}
|
|
|
|
/// Move cursor to start of the buffer
|
|
pub fn move_home(&mut self) {
|
|
self.cursor = 0;
|
|
}
|
|
|
|
/// Move cursor to end of the buffer
|
|
pub fn move_end(&mut self) {
|
|
self.cursor = self.buffer.len();
|
|
}
|
|
|
|
/// Push current buffer into history, clearing the buffer afterwards
|
|
pub fn commit_to_history(&mut self) -> String {
|
|
let text = std::mem::take(&mut self.buffer);
|
|
if !text.trim().is_empty() {
|
|
self.push_history_entry(text.clone());
|
|
}
|
|
self.cursor = 0;
|
|
self.history_index = None;
|
|
text
|
|
}
|
|
|
|
/// Navigate to previous history entry
|
|
pub fn history_previous(&mut self) {
|
|
if self.history.is_empty() {
|
|
return;
|
|
}
|
|
|
|
let new_index = match self.history_index {
|
|
Some(idx) if idx + 1 < self.history.len() => idx + 1,
|
|
None => 0,
|
|
_ => return,
|
|
};
|
|
|
|
self.history_index = Some(new_index);
|
|
if let Some(entry) = self.history.get(new_index) {
|
|
self.buffer = entry.clone();
|
|
self.cursor = self.buffer.len();
|
|
}
|
|
}
|
|
|
|
/// Navigate to next history entry
|
|
pub fn history_next(&mut self) {
|
|
if self.history.is_empty() {
|
|
return;
|
|
}
|
|
|
|
if let Some(idx) = self.history_index {
|
|
if idx > 0 {
|
|
let new_idx = idx - 1;
|
|
self.history_index = Some(new_idx);
|
|
if let Some(entry) = self.history.get(new_idx) {
|
|
self.buffer = entry.clone();
|
|
self.cursor = self.buffer.len();
|
|
}
|
|
} else {
|
|
self.history_index = None;
|
|
self.buffer.clear();
|
|
self.cursor = 0;
|
|
}
|
|
} else {
|
|
self.buffer.clear();
|
|
self.cursor = 0;
|
|
}
|
|
}
|
|
|
|
/// Push a new entry into the history buffer, enforcing capacity
|
|
pub fn push_history_entry(&mut self, entry: String) {
|
|
if self
|
|
.history
|
|
.front()
|
|
.map(|existing| existing == &entry)
|
|
.unwrap_or(false)
|
|
{
|
|
return;
|
|
}
|
|
|
|
self.history.push_front(entry);
|
|
while self.history.len() > self.max_history {
|
|
self.history.pop_back();
|
|
}
|
|
}
|
|
|
|
/// Clear saved input history entries.
|
|
pub fn clear_history(&mut self) {
|
|
self.history.clear();
|
|
self.history_index = None;
|
|
}
|
|
}
|
|
|
|
fn prev_char_boundary(buffer: &str, cursor: usize) -> usize {
|
|
buffer[..cursor]
|
|
.char_indices()
|
|
.last()
|
|
.map(|(idx, _)| idx)
|
|
.unwrap_or(0)
|
|
}
|
|
|
|
fn next_char_boundary(buffer: &str, cursor: usize) -> usize {
|
|
if cursor >= buffer.len() {
|
|
return buffer.len();
|
|
}
|
|
|
|
let slice = &buffer[cursor..];
|
|
let mut iter = slice.char_indices();
|
|
iter.next();
|
|
if let Some((idx, _)) = iter.next() {
|
|
cursor + idx
|
|
} else {
|
|
buffer.len()
|
|
}
|
|
}
|