#[derive(Debug, Clone)]
pub struct TextBuffer {
text: String,
cursor: usize, }
impl TextBuffer {
pub fn new(initial: &str) -> Self {
let cursor = initial.chars().count();
Self {
text: initial.to_string(),
cursor,
}
}
pub fn text(&self) -> &str {
&self.text
}
#[cfg(test)]
pub fn cursor(&self) -> usize {
self.cursor
}
#[cfg(test)]
pub fn set_cursor(&mut self, pos: usize) {
self.cursor = pos.min(self.len());
}
pub fn len(&self) -> usize {
self.text.chars().count()
}
pub fn insert(&mut self, c: char) {
let byte_pos = self.cursor_byte_pos();
self.text.insert(byte_pos, c);
self.cursor += 1;
}
pub fn backspace(&mut self) -> bool {
if self.cursor > 0 {
self.cursor -= 1;
let byte_pos = self.cursor_byte_pos();
self.text.remove(byte_pos);
true
} else {
false
}
}
pub fn delete(&mut self) -> bool {
if self.cursor < self.len() {
let byte_pos = self.cursor_byte_pos();
self.text.remove(byte_pos);
true
} else {
false
}
}
pub fn move_left(&mut self) {
self.cursor = self.cursor.saturating_sub(1);
}
pub fn move_right(&mut self) {
self.cursor = (self.cursor + 1).min(self.len());
}
pub fn move_to_start(&mut self) {
self.cursor = 0;
}
pub fn move_to_end(&mut self) {
self.cursor = self.len();
}
pub fn cursor_byte_pos(&self) -> usize {
self.char_to_byte_index(self.cursor)
}
fn char_to_byte_index(&self, char_index: usize) -> usize {
self.text
.char_indices()
.nth(char_index)
.map(|(i, _)| i)
.unwrap_or(self.text.len())
}
pub fn delete_word_back(&mut self) -> bool {
if self.cursor == 0 {
return false;
}
let chars: Vec<char> = self.text.chars().collect();
let mut new_cursor = self.cursor;
while new_cursor > 0 && (chars[new_cursor - 1] == '.' || chars[new_cursor - 1] == ' ') {
new_cursor -= 1;
}
while new_cursor > 0 && chars[new_cursor - 1] != '.' && chars[new_cursor - 1] != ' ' {
new_cursor -= 1;
}
if new_cursor < self.cursor {
let start_byte = self.char_to_byte_index(new_cursor);
let end_byte = self.char_to_byte_index(self.cursor);
self.text.replace_range(start_byte..end_byte, "");
self.cursor = new_cursor;
true
} else {
false
}
}
pub fn navigation_positions(&self) -> Vec<usize> {
Self::compute_navigation_positions(&self.text)
}
fn compute_navigation_positions(filter_text: &str) -> Vec<usize> {
let mut positions = vec![0];
let mut in_string = false;
let mut prev_char = ' ';
let chars: Vec<char> = filter_text.chars().collect();
let len = chars.len();
for (i, &c) in chars.iter().enumerate() {
if c == '"' && prev_char != '\\' {
in_string = !in_string;
}
if !in_string {
if prev_char == ']' {
positions.push(i);
}
if c == '|' {
positions.push(i);
}
if (prev_char == '|' || (prev_char == ' ' && i > 1 && chars[i - 2] == '|'))
&& c != ' '
{
positions.push(i);
}
if i > 0 && !c.is_alphanumeric() && c != '_' {
let mut j = i;
while j > 0 && (chars[j - 1].is_alphanumeric() || chars[j - 1] == '_') {
j -= 1;
}
if j > 0 && chars[j - 1] == '.' && j < i {
positions.push(i);
}
}
}
prev_char = c;
}
if len > 0 {
let last = chars.last().copied().unwrap_or(' ');
if (len == 1 && last == '.') || last == ']' || last.is_alphanumeric() || last == '_' {
positions.push(len);
}
}
if len > 1 && filter_text.starts_with('.') {
positions.push(1);
}
positions.sort();
positions.dedup();
positions
}
pub fn jump_word_back(&mut self) {
if self.cursor == 0 {
return;
}
let positions = self.navigation_positions();
let mut target = 0;
for &pos in &positions {
if pos < self.cursor {
target = pos;
}
}
self.cursor = target;
}
pub fn jump_word_forward(&mut self) {
let len = self.len();
if self.cursor >= len {
return;
}
let positions = self.navigation_positions();
for &pos in &positions {
if pos > self.cursor {
self.cursor = pos;
return;
}
}
self.cursor = len;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new() {
let buf = TextBuffer::new("hello");
assert_eq!(buf.text(), "hello");
assert_eq!(buf.cursor(), 5);
assert_eq!(buf.len(), 5);
}
#[test]
fn test_insert() {
let mut buf = TextBuffer::new(".");
buf.insert('f');
assert_eq!(buf.text(), ".f");
buf.insert('o');
buf.insert('o');
assert_eq!(buf.text(), ".foo");
assert_eq!(buf.cursor(), 4);
}
#[test]
fn test_backspace() {
let mut buf = TextBuffer::new(".foo");
assert!(buf.backspace());
assert_eq!(buf.text(), ".fo");
assert_eq!(buf.cursor(), 3);
}
#[test]
fn test_backspace_at_start() {
let mut buf = TextBuffer::new(".");
buf.set_cursor(0);
assert!(!buf.backspace());
assert_eq!(buf.text(), ".");
}
#[test]
fn test_delete() {
let mut buf = TextBuffer::new(".foo");
buf.set_cursor(1);
assert!(buf.delete());
assert_eq!(buf.text(), ".oo");
assert_eq!(buf.cursor(), 1);
}
#[test]
fn test_delete_at_end() {
let mut buf = TextBuffer::new(".foo");
assert!(!buf.delete());
assert_eq!(buf.text(), ".foo");
}
#[test]
fn test_move_left_right() {
let mut buf = TextBuffer::new(".foo");
assert_eq!(buf.cursor(), 4);
buf.move_left();
assert_eq!(buf.cursor(), 3);
buf.move_right();
assert_eq!(buf.cursor(), 4);
buf.move_right(); assert_eq!(buf.cursor(), 4);
}
#[test]
fn test_move_to_start_end() {
let mut buf = TextBuffer::new(".foo");
buf.move_to_start();
assert_eq!(buf.cursor(), 0);
buf.move_to_end();
assert_eq!(buf.cursor(), 4);
}
#[test]
fn test_delete_word_back() {
let mut buf = TextBuffer::new(".foo.bar");
assert!(buf.delete_word_back());
assert_eq!(buf.text(), ".foo.");
assert_eq!(buf.cursor(), 5);
}
#[test]
fn test_delete_word_back_at_start() {
let mut buf = TextBuffer::new(".");
buf.set_cursor(0);
assert!(!buf.delete_word_back());
assert_eq!(buf.text(), ".");
}
#[test]
fn test_navigation_positions_identity() {
let buf = TextBuffer::new(".");
assert_eq!(buf.navigation_positions(), vec![0, 1]);
}
#[test]
fn test_navigation_positions_field() {
let buf = TextBuffer::new(".foo");
assert_eq!(buf.navigation_positions(), vec![0, 1, 4]);
}
#[test]
fn test_navigation_positions_iterate() {
let buf = TextBuffer::new(".[]");
assert_eq!(buf.navigation_positions(), vec![0, 1, 3]);
}
#[test]
fn test_navigation_positions_multiple_iterate() {
let buf = TextBuffer::new(".[][][]");
assert_eq!(buf.navigation_positions(), vec![0, 1, 3, 5, 7]);
}
#[test]
fn test_navigation_positions_index() {
let buf = TextBuffer::new(".[0]");
assert_eq!(buf.navigation_positions(), vec![0, 1, 4]);
}
#[test]
fn test_navigation_positions_field_chain() {
let buf = TextBuffer::new(".foo.bar");
assert_eq!(buf.navigation_positions(), vec![0, 1, 4, 8]);
}
#[test]
fn test_navigation_positions_mixed() {
let buf = TextBuffer::new(".foo[0].bar");
assert_eq!(buf.navigation_positions(), vec![0, 1, 4, 7, 11]);
}
#[test]
fn test_navigation_positions_quoted_field() {
let buf = TextBuffer::new(".[\"foo\"]");
assert_eq!(buf.navigation_positions(), vec![0, 1, 8]);
}
#[test]
fn test_jump_word_back() {
let mut buf = TextBuffer::new(".foo.bar");
buf.jump_word_back();
assert_eq!(buf.cursor(), 4);
buf.jump_word_back();
assert_eq!(buf.cursor(), 1);
buf.jump_word_back();
assert_eq!(buf.cursor(), 0);
}
#[test]
fn test_jump_word_forward() {
let mut buf = TextBuffer::new(".foo.bar");
buf.set_cursor(0);
buf.jump_word_forward();
assert_eq!(buf.cursor(), 1);
buf.jump_word_forward();
assert_eq!(buf.cursor(), 4);
buf.jump_word_forward();
assert_eq!(buf.cursor(), 8);
}
#[test]
fn test_unicode_handling() {
let mut buf = TextBuffer::new(".héllo");
assert_eq!(buf.len(), 6);
assert_eq!(buf.cursor(), 6);
buf.move_left();
assert_eq!(buf.cursor(), 5);
buf.insert('!');
assert_eq!(buf.text(), ".héll!o");
}
#[test]
fn test_navigation_positions_with_pipes() {
let buf = TextBuffer::new(". | sort | reverse");
let positions = buf.navigation_positions();
assert!(positions.contains(&0)); assert!(positions.contains(&1)); assert!(positions.contains(&2)); assert!(positions.contains(&4)); assert!(positions.contains(&9)); assert!(positions.contains(&11)); assert!(positions.contains(&18)); }
#[test]
fn test_jump_word_back_with_pipes() {
let mut buf = TextBuffer::new(". | sort | reverse");
buf.jump_word_back();
assert_eq!(buf.cursor(), 11); buf.jump_word_back();
assert_eq!(buf.cursor(), 9); buf.jump_word_back();
assert_eq!(buf.cursor(), 4); buf.jump_word_back();
assert_eq!(buf.cursor(), 2); buf.jump_word_back();
assert_eq!(buf.cursor(), 1); buf.jump_word_back();
assert_eq!(buf.cursor(), 0); }
}