use crate::primitives::word_navigation::{find_word_end_bytes, find_word_start_bytes};
#[derive(Debug, Clone)]
pub struct TextEdit {
pub lines: Vec<String>,
pub cursor_row: usize,
pub cursor_col: usize,
pub selection_anchor: Option<(usize, usize)>,
pub multiline: bool,
}
impl Default for TextEdit {
fn default() -> Self {
Self::new()
}
}
impl TextEdit {
pub fn new() -> Self {
Self {
lines: vec![String::new()],
cursor_row: 0,
cursor_col: 0,
selection_anchor: None,
multiline: true,
}
}
pub fn single_line() -> Self {
Self {
lines: vec![String::new()],
cursor_row: 0,
cursor_col: 0,
selection_anchor: None,
multiline: false,
}
}
pub fn with_text(text: &str) -> Self {
let lines: Vec<String> = text.lines().map(String::from).collect();
let lines = if lines.is_empty() {
vec![String::new()]
} else {
lines
};
Self {
lines,
cursor_row: 0,
cursor_col: 0,
selection_anchor: None,
multiline: true,
}
}
pub fn single_line_with_text(text: &str) -> Self {
let first_line = text.lines().next().unwrap_or("").to_string();
Self {
lines: vec![first_line],
cursor_row: 0,
cursor_col: 0,
selection_anchor: None,
multiline: false,
}
}
pub fn value(&self) -> String {
self.lines.join("\n")
}
pub fn set_value(&mut self, text: &str) {
if self.multiline {
self.lines = text.lines().map(String::from).collect();
if self.lines.is_empty() {
self.lines.push(String::new());
}
} else {
self.lines = vec![text.lines().next().unwrap_or("").to_string()];
}
self.cursor_row = 0;
self.cursor_col = 0;
self.selection_anchor = None;
}
pub fn current_line(&self) -> &str {
self.lines
.get(self.cursor_row)
.map(|s| s.as_str())
.unwrap_or("")
}
pub fn line_count(&self) -> usize {
self.lines.len()
}
pub fn move_left(&mut self) {
self.clear_selection();
self.move_left_internal();
}
fn move_left_internal(&mut self) {
if self.cursor_col > 0 {
let line = &self.lines[self.cursor_row];
let mut new_col = self.cursor_col - 1;
while new_col > 0 && !line.is_char_boundary(new_col) {
new_col -= 1;
}
self.cursor_col = new_col;
} else if self.cursor_row > 0 && self.multiline {
self.cursor_row -= 1;
self.cursor_col = self.lines[self.cursor_row].len();
}
}
pub fn move_right(&mut self) {
self.clear_selection();
self.move_right_internal();
}
fn move_right_internal(&mut self) {
let line_len = self
.lines
.get(self.cursor_row)
.map(|l| l.len())
.unwrap_or(0);
if self.cursor_col < line_len {
let line = &self.lines[self.cursor_row];
let mut new_col = self.cursor_col + 1;
while new_col < line.len() && !line.is_char_boundary(new_col) {
new_col += 1;
}
self.cursor_col = new_col;
} else if self.cursor_row + 1 < self.lines.len() && self.multiline {
self.cursor_row += 1;
self.cursor_col = 0;
}
}
pub fn move_up(&mut self) {
self.clear_selection();
self.move_up_internal();
}
fn move_up_internal(&mut self) {
if self.cursor_row > 0 {
self.cursor_row -= 1;
let line_len = self.lines[self.cursor_row].len();
self.cursor_col = self.cursor_col.min(line_len);
}
}
pub fn move_down(&mut self) {
self.clear_selection();
self.move_down_internal();
}
fn move_down_internal(&mut self) {
if self.cursor_row + 1 < self.lines.len() {
self.cursor_row += 1;
let line_len = self.lines[self.cursor_row].len();
self.cursor_col = self.cursor_col.min(line_len);
}
}
pub fn move_home(&mut self) {
self.clear_selection();
self.cursor_col = 0;
}
pub fn move_end(&mut self) {
self.clear_selection();
self.cursor_col = self
.lines
.get(self.cursor_row)
.map(|l| l.len())
.unwrap_or(0);
}
pub fn move_word_left(&mut self) {
self.clear_selection();
self.move_word_left_internal();
}
fn move_word_left_internal(&mut self) {
let line = &self.lines[self.cursor_row];
if self.cursor_col > 0 {
let new_col = find_word_start_bytes(line.as_bytes(), self.cursor_col);
if new_col < self.cursor_col {
self.cursor_col = new_col;
return;
}
}
if self.cursor_row > 0 && self.multiline {
self.cursor_row -= 1;
self.cursor_col = self.lines[self.cursor_row].len();
}
}
pub fn move_word_right(&mut self) {
self.clear_selection();
self.move_word_right_internal();
}
fn move_word_right_internal(&mut self) {
let line = &self.lines[self.cursor_row];
if self.cursor_col < line.len() {
let new_col = find_word_end_bytes(line.as_bytes(), self.cursor_col);
if new_col > self.cursor_col {
self.cursor_col = new_col;
return;
}
}
if self.cursor_row + 1 < self.lines.len() && self.multiline {
self.cursor_row += 1;
self.cursor_col = 0;
}
}
pub fn has_selection(&self) -> bool {
if let Some((anchor_row, anchor_col)) = self.selection_anchor {
anchor_row != self.cursor_row || anchor_col != self.cursor_col
} else {
false
}
}
pub fn selection_range(&self) -> Option<((usize, usize), (usize, usize))> {
let (anchor_row, anchor_col) = self.selection_anchor?;
if anchor_row == self.cursor_row && anchor_col == self.cursor_col {
return None;
}
let (start, end) = if anchor_row < self.cursor_row
|| (anchor_row == self.cursor_row && anchor_col < self.cursor_col)
{
((anchor_row, anchor_col), (self.cursor_row, self.cursor_col))
} else {
((self.cursor_row, self.cursor_col), (anchor_row, anchor_col))
};
Some((start, end))
}
pub fn selected_text(&self) -> Option<String> {
let ((start_row, start_col), (end_row, end_col)) = self.selection_range()?;
if start_row == end_row {
let line = &self.lines[start_row];
let end_col = end_col.min(line.len());
let start_col = start_col.min(end_col);
Some(line[start_col..end_col].to_string())
} else {
let mut result = String::new();
let first_line = &self.lines[start_row];
result.push_str(&first_line[start_col.min(first_line.len())..]);
result.push('\n');
for row in (start_row + 1)..end_row {
result.push_str(&self.lines[row]);
result.push('\n');
}
let last_line = &self.lines[end_row];
result.push_str(&last_line[..end_col.min(last_line.len())]);
Some(result)
}
}
pub fn delete_selection(&mut self) -> Option<String> {
let ((start_row, start_col), (end_row, end_col)) = self.selection_range()?;
let deleted = self.selected_text()?;
if start_row == end_row {
let line = &mut self.lines[start_row];
let end_col = end_col.min(line.len());
let start_col = start_col.min(end_col);
line.drain(start_col..end_col);
} else {
let end_col = end_col.min(self.lines[end_row].len());
let after_end = self.lines[end_row][end_col..].to_string();
self.lines[start_row].truncate(start_col);
self.lines[start_row].push_str(&after_end);
for _ in (start_row + 1)..=end_row {
self.lines.remove(start_row + 1);
}
}
self.cursor_row = start_row;
self.cursor_col = start_col;
self.selection_anchor = None;
Some(deleted)
}
pub fn clear_selection(&mut self) {
self.selection_anchor = None;
}
fn ensure_anchor(&mut self) {
if self.selection_anchor.is_none() {
self.selection_anchor = Some((self.cursor_row, self.cursor_col));
}
}
pub fn move_left_selecting(&mut self) {
self.ensure_anchor();
self.move_left_internal();
}
pub fn move_right_selecting(&mut self) {
self.ensure_anchor();
self.move_right_internal();
}
pub fn move_up_selecting(&mut self) {
self.ensure_anchor();
self.move_up_internal();
}
pub fn move_down_selecting(&mut self) {
self.ensure_anchor();
self.move_down_internal();
}
pub fn move_home_selecting(&mut self) {
self.ensure_anchor();
self.cursor_col = 0;
}
pub fn move_end_selecting(&mut self) {
self.ensure_anchor();
self.cursor_col = self
.lines
.get(self.cursor_row)
.map(|l| l.len())
.unwrap_or(0);
}
pub fn move_word_left_selecting(&mut self) {
self.ensure_anchor();
self.move_word_left_internal();
}
pub fn move_word_right_selecting(&mut self) {
self.ensure_anchor();
self.move_word_right_internal();
}
pub fn select_all(&mut self) {
self.selection_anchor = Some((0, 0));
self.cursor_row = self.lines.len().saturating_sub(1);
self.cursor_col = self.lines.last().map(|l| l.len()).unwrap_or(0);
}
pub fn insert_char(&mut self, c: char) {
if self.has_selection() {
self.delete_selection();
}
if c == '\n' && self.multiline {
let current_line = &self.lines[self.cursor_row];
let col = self.cursor_col.min(current_line.len());
let (before, after) = current_line.split_at(col);
let before = before.to_string();
let after = after.to_string();
self.lines[self.cursor_row] = before;
self.lines.insert(self.cursor_row + 1, after);
self.cursor_row += 1;
self.cursor_col = 0;
} else if c != '\n' {
if self.cursor_row < self.lines.len() {
let line = &mut self.lines[self.cursor_row];
let col = self.cursor_col.min(line.len());
line.insert(col, c);
self.cursor_col = col + c.len_utf8();
}
}
}
pub fn insert_str(&mut self, text: &str) {
if self.has_selection() {
self.delete_selection();
}
for c in text.chars() {
if c == '\n' && !self.multiline {
continue;
}
self.insert_char(c);
}
}
pub fn backspace(&mut self) {
if self.has_selection() {
self.delete_selection();
return;
}
if self.cursor_col > 0 {
let line = &mut self.lines[self.cursor_row];
let mut del_start = self.cursor_col - 1;
while del_start > 0 && !line.is_char_boundary(del_start) {
del_start -= 1;
}
line.drain(del_start..self.cursor_col);
self.cursor_col = del_start;
} else if self.cursor_row > 0 && self.multiline {
let current_line = self.lines.remove(self.cursor_row);
self.cursor_row -= 1;
self.cursor_col = self.lines[self.cursor_row].len();
self.lines[self.cursor_row].push_str(¤t_line);
}
}
pub fn delete(&mut self) {
if self.has_selection() {
self.delete_selection();
return;
}
let line_len = self
.lines
.get(self.cursor_row)
.map(|l| l.len())
.unwrap_or(0);
if self.cursor_col < line_len {
let line = &mut self.lines[self.cursor_row];
let mut del_end = self.cursor_col + 1;
while del_end < line.len() && !line.is_char_boundary(del_end) {
del_end += 1;
}
line.drain(self.cursor_col..del_end);
} else if self.cursor_row + 1 < self.lines.len() && self.multiline {
let next_line = self.lines.remove(self.cursor_row + 1);
self.lines[self.cursor_row].push_str(&next_line);
}
}
pub fn delete_word_forward(&mut self) {
if self.has_selection() {
self.delete_selection();
return;
}
let line = &self.lines[self.cursor_row];
let word_end = find_word_end_bytes(line.as_bytes(), self.cursor_col);
if word_end > self.cursor_col {
let line = &mut self.lines[self.cursor_row];
line.drain(self.cursor_col..word_end);
} else if self.cursor_row + 1 < self.lines.len() && self.multiline {
let next_line = self.lines.remove(self.cursor_row + 1);
self.lines[self.cursor_row].push_str(&next_line);
}
}
pub fn delete_word_backward(&mut self) {
if self.has_selection() {
self.delete_selection();
return;
}
let line = &self.lines[self.cursor_row];
let word_start = find_word_start_bytes(line.as_bytes(), self.cursor_col);
if word_start < self.cursor_col {
let line = &mut self.lines[self.cursor_row];
line.drain(word_start..self.cursor_col);
self.cursor_col = word_start;
} else if self.cursor_row > 0 && self.multiline {
let current_line = self.lines.remove(self.cursor_row);
self.cursor_row -= 1;
self.cursor_col = self.lines[self.cursor_row].len();
self.lines[self.cursor_row].push_str(¤t_line);
}
}
pub fn delete_to_end(&mut self) {
if self.has_selection() {
self.delete_selection();
return;
}
if let Some(line) = self.lines.get_mut(self.cursor_row) {
line.truncate(self.cursor_col);
}
}
pub fn clear(&mut self) {
self.lines = vec![String::new()];
self.cursor_row = 0;
self.cursor_col = 0;
self.selection_anchor = None;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_single_line_basic() {
let mut edit = TextEdit::single_line();
edit.insert_str("hello world");
assert_eq!(edit.value(), "hello world");
assert_eq!(edit.cursor_col, 11);
}
#[test]
fn test_single_line_ignores_newlines() {
let mut edit = TextEdit::single_line();
edit.insert_str("hello\nworld");
assert_eq!(edit.value(), "helloworld");
assert_eq!(edit.line_count(), 1);
}
#[test]
fn test_multiline_basic() {
let mut edit = TextEdit::new();
edit.insert_str("hello\nworld");
assert_eq!(edit.value(), "hello\nworld");
assert_eq!(edit.line_count(), 2);
assert_eq!(edit.cursor_row, 1);
assert_eq!(edit.cursor_col, 5);
}
#[test]
fn test_selection_single_line() {
let mut edit = TextEdit::single_line_with_text("hello world");
edit.cursor_col = 6;
edit.move_right_selecting();
edit.move_right_selecting();
edit.move_right_selecting();
edit.move_right_selecting();
edit.move_right_selecting();
assert!(edit.has_selection());
assert_eq!(edit.selected_text(), Some("world".to_string()));
}
#[test]
fn test_selection_multiline() {
let mut edit = TextEdit::with_text("line1\nline2\nline3");
edit.cursor_row = 0;
edit.cursor_col = 3;
edit.move_down_selecting();
edit.move_right_selecting();
edit.move_right_selecting();
assert!(edit.has_selection());
let selected = edit.selected_text().unwrap();
assert_eq!(selected, "e1\nline2");
}
#[test]
fn test_delete_selection() {
let mut edit = TextEdit::with_text("hello world");
edit.cursor_col = 0;
for _ in 0..6 {
edit.move_right_selecting();
}
let deleted = edit.delete_selection();
assert_eq!(deleted, Some("hello ".to_string()));
assert_eq!(edit.value(), "world");
assert_eq!(edit.cursor_col, 0);
}
#[test]
fn test_backspace_with_selection() {
let mut edit = TextEdit::with_text("hello world");
edit.select_all();
edit.backspace();
assert_eq!(edit.value(), "");
}
#[test]
fn test_insert_replaces_selection() {
let mut edit = TextEdit::with_text("hello world");
edit.select_all();
edit.insert_str("goodbye");
assert_eq!(edit.value(), "goodbye");
}
#[test]
fn test_word_navigation() {
let mut edit = TextEdit::single_line_with_text("one two three");
edit.cursor_col = 0;
edit.move_word_right();
assert_eq!(edit.cursor_col, 3);
edit.move_word_right();
assert_eq!(edit.cursor_col, 7);
edit.move_word_left();
assert_eq!(edit.cursor_col, 4); }
}