#[derive(Debug, Clone, Default)]
pub struct LineEditor {
chars: Vec<char>,
cursor: usize,
anchor: Option<usize>,
clipboard: String,
multiline: bool,
}
impl LineEditor {
pub fn new(initial: &str, multiline: bool) -> Self {
let chars: Vec<char> = initial.chars().collect();
let cursor = chars.len();
Self {
chars,
cursor,
anchor: None,
clipboard: String::new(),
multiline,
}
}
pub fn value(&self) -> String {
self.chars.iter().collect()
}
pub fn set_value(&mut self, value: &str) {
self.chars = value.chars().collect();
self.cursor = self.chars.len();
self.anchor = None;
}
pub fn len(&self) -> usize {
self.chars.len()
}
pub fn cursor(&self) -> usize {
self.cursor
}
pub fn lines(&self) -> Vec<String> {
self.value().split('\n').map(str::to_string).collect()
}
pub fn cursor_line_col(&self) -> (usize, usize) {
let mut line = 0;
let mut col = 0;
for &ch in &self.chars[..self.cursor] {
if ch == '\n' {
line += 1;
col = 0;
} else {
col += 1;
}
}
(line, col)
}
pub fn selection_range(&self) -> Option<(usize, usize)> {
let anchor = self.anchor?;
if anchor == self.cursor {
return None;
}
Some((anchor.min(self.cursor), anchor.max(self.cursor)))
}
pub fn insert_char(&mut self, ch: char) {
self.delete_selection();
self.chars.insert(self.cursor, ch);
self.cursor += 1;
}
pub fn insert_str(&mut self, text: &str) {
self.delete_selection();
for ch in text.chars() {
let ch = if !self.multiline && ch == '\n' {
' '
} else {
ch
};
self.chars.insert(self.cursor, ch);
self.cursor += 1;
}
}
pub fn insert_newline(&mut self) {
if self.multiline {
self.insert_char('\n');
}
}
pub fn backspace(&mut self) {
if self.delete_selection() {
return;
}
if self.cursor > 0 {
self.cursor -= 1;
self.chars.remove(self.cursor);
}
}
pub fn delete(&mut self) {
if self.delete_selection() {
return;
}
if self.cursor < self.chars.len() {
self.chars.remove(self.cursor);
}
}
pub fn move_left(&mut self, select: bool) {
self.update_anchor(select);
if self.cursor > 0 {
self.cursor -= 1;
}
}
pub fn move_right(&mut self, select: bool) {
self.update_anchor(select);
if self.cursor < self.chars.len() {
self.cursor += 1;
}
}
pub fn move_home(&mut self, select: bool) {
self.update_anchor(select);
self.cursor = self.line_start(self.cursor);
}
pub fn move_end(&mut self, select: bool) {
self.update_anchor(select);
self.cursor = self.line_end(self.cursor);
}
pub fn move_up(&mut self, select: bool) {
self.update_anchor(select);
let start = self.line_start(self.cursor);
if start == 0 {
return;
}
let col = self.cursor - start;
let prev_start = self.line_start(start - 1);
let prev_end = start - 1;
self.cursor = (prev_start + col).min(prev_end);
}
pub fn move_down(&mut self, select: bool) {
self.update_anchor(select);
let end = self.line_end(self.cursor);
if end >= self.chars.len() {
return;
}
let col = self.cursor - self.line_start(self.cursor);
let next_start = end + 1;
let next_end = self.line_end(next_start);
self.cursor = (next_start + col).min(next_end);
}
pub fn select_all(&mut self) {
self.anchor = Some(0);
self.cursor = self.chars.len();
}
pub fn delete_word_back(&mut self) {
if self.delete_selection() {
return;
}
let mut index = self.cursor;
while index > 0 && self.chars[index - 1].is_whitespace() {
index -= 1;
}
while index > 0 && !self.chars[index - 1].is_whitespace() {
index -= 1;
}
self.chars.drain(index..self.cursor);
self.cursor = index;
}
pub fn kill_to_line_start(&mut self) {
let start = self.line_start(self.cursor);
self.chars.drain(start..self.cursor);
self.cursor = start;
}
pub fn kill_to_line_end(&mut self) {
let end = self.line_end(self.cursor);
self.chars.drain(self.cursor..end);
}
pub fn copy(&mut self) {
if let Some((start, end)) = self.selection_range() {
self.clipboard = self.chars[start..end].iter().collect();
}
}
pub fn cut(&mut self) {
self.copy();
self.delete_selection();
}
pub fn paste(&mut self) {
let text = std::mem::take(&mut self.clipboard);
self.insert_str(&text);
self.clipboard = text;
}
fn update_anchor(&mut self, select: bool) {
if select {
if self.anchor.is_none() {
self.anchor = Some(self.cursor);
}
} else {
self.anchor = None;
}
}
fn delete_selection(&mut self) -> bool {
let Some((start, end)) = self.selection_range() else {
self.anchor = None;
return false;
};
self.chars.drain(start..end);
self.cursor = start;
self.anchor = None;
true
}
fn line_start(&self, index: usize) -> usize {
self.chars[..index]
.iter()
.rposition(|&c| c == '\n')
.map_or(0, |pos| pos + 1)
}
fn line_end(&self, index: usize) -> usize {
self.chars[index..]
.iter()
.position(|&c| c == '\n')
.map_or(self.chars.len(), |offset| index + offset)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn inserts_and_reports_value() {
let mut editor = LineEditor::new("", false);
editor.insert_char('h');
editor.insert_char('i');
assert_eq!(editor.value(), "hi");
assert_eq!(editor.cursor(), 2);
}
#[test]
fn backspace_removes_previous_char() {
let mut editor = LineEditor::new("ab", false);
editor.backspace();
assert_eq!(editor.value(), "a");
}
#[test]
fn selection_is_replaced_on_insert() {
let mut editor = LineEditor::new("hello", false);
editor.move_home(false);
editor.move_right(true);
editor.move_right(true);
editor.insert_char('X');
assert_eq!(editor.value(), "Xllo");
}
#[test]
fn delete_word_back_removes_word() {
let mut editor = LineEditor::new("foo bar", false);
editor.delete_word_back();
assert_eq!(editor.value(), "foo ");
}
#[test]
fn kill_to_line_start_and_end() {
let mut editor = LineEditor::new("hello", false);
editor.move_home(false);
editor.move_right(false);
editor.move_right(false);
editor.kill_to_line_end();
assert_eq!(editor.value(), "he");
editor.kill_to_line_start();
assert_eq!(editor.value(), "");
}
#[test]
fn single_line_converts_pasted_newlines() {
let mut editor = LineEditor::new("", false);
editor.insert_str("a\nb");
assert_eq!(editor.value(), "a b");
}
#[test]
fn multiline_navigates_between_rows() {
let mut editor = LineEditor::new("ab\ncd", true);
editor.move_home(false);
editor.move_up(false);
assert_eq!(editor.cursor_line_col(), (0, 0));
}
#[test]
fn cut_and_paste_round_trip() {
let mut editor = LineEditor::new("hello", false);
editor.select_all();
editor.cut();
assert_eq!(editor.value(), "");
editor.paste();
assert_eq!(editor.value(), "hello");
}
}