#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Edit {
Insert(char),
Newline,
Backspace,
Delete,
Left,
Right,
Home,
End,
WordLeft,
WordRight,
DeleteWord,
Older,
Newer,
Clear,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Layout {
pub(crate) cursor_row: u16,
pub(crate) cursor_column: u16,
pub(crate) end_row: u16,
}
pub(crate) const PROMPT: &str = "> ";
#[derive(Debug, Default)]
pub(crate) struct Editor {
text: Vec<char>,
cursor: usize,
history: Vec<String>,
recalled: Option<usize>,
draft: Vec<char>,
}
impl Editor {
pub(crate) fn is_empty(&self) -> bool {
self.text.is_empty()
}
pub(crate) fn text(&self) -> String {
self.text.iter().collect()
}
pub(crate) fn set(&mut self, text: &str) {
self.text = text.chars().collect();
self.cursor = self.text.len();
}
pub(crate) fn apply(&mut self, edit: Edit) {
match edit {
Edit::Insert(ch) => self.insert(ch),
Edit::Newline => self.insert('\n'),
Edit::Backspace => {
if let Some(before) = self.cursor.checked_sub(1) {
self.text.remove(before);
self.cursor = before;
}
}
Edit::Delete => {
if self.cursor < self.text.len() {
self.text.remove(self.cursor);
}
}
Edit::Left => self.cursor = self.cursor.saturating_sub(1),
Edit::Right => self.cursor = self.cursor.saturating_add(1).min(self.text.len()),
Edit::Home => self.cursor = 0,
Edit::End => self.cursor = self.text.len(),
Edit::WordLeft => self.cursor = self.word_start(),
Edit::WordRight => self.cursor = self.word_end(),
Edit::DeleteWord => {
let start = self.word_start();
self.text.drain(start..self.cursor);
self.cursor = start;
}
Edit::Older => self.recall(self.recalled.map_or(0, |n| n.saturating_add(1))),
Edit::Newer => match self.recalled {
None => {}
Some(0) => {
self.recalled = None;
self.text = std::mem::take(&mut self.draft);
self.cursor = self.text.len();
}
Some(n) => self.recall(n.saturating_sub(1)),
},
Edit::Clear => {
self.text.clear();
self.cursor = 0;
self.recalled = None;
}
}
}
pub(crate) fn enter(&mut self) -> Option<String> {
if self.cursor == self.text.len() && self.text.last() == Some(&'\\') {
self.text.pop();
self.cursor = self.text.len();
self.insert('\n');
return None;
}
let line = self.text();
self.apply(Edit::Clear);
if !line.trim().is_empty() && self.history.last() != Some(&line) {
self.history.push(line.clone());
}
Some(line)
}
pub(crate) fn layout(&self, width: u16) -> Layout {
let width = usize::from(width.max(1));
let start = PROMPT.chars().count();
let (mut row, mut column) = (0usize, start);
let mut at_cursor = (row, column);
for (index, ch) in self.text.iter().enumerate() {
if index == self.cursor {
at_cursor = (row, column);
}
if *ch == '\n' {
row = row.saturating_add(1);
column = start;
continue;
}
let wide = columns(*ch);
if column.saturating_add(wide) > width {
row = row.saturating_add(1);
column = 0;
}
column = column.saturating_add(wide);
}
if self.cursor >= self.text.len() {
at_cursor = (row, column);
}
let narrow = |n: usize| u16::try_from(n).unwrap_or(u16::MAX);
Layout {
cursor_row: narrow(at_cursor.0),
cursor_column: narrow(at_cursor.1),
end_row: narrow(row),
}
}
fn insert(&mut self, ch: char) {
self.text.insert(self.cursor, ch);
self.cursor = self.cursor.saturating_add(1);
}
fn recall(&mut self, back: usize) {
let Some(index) = self.history.len().checked_sub(back.saturating_add(1)) else {
return;
};
let Some(line) = self.history.get(index) else {
return;
};
if self.recalled.is_none() {
self.draft = std::mem::take(&mut self.text);
}
self.text = line.chars().collect();
self.cursor = self.text.len();
self.recalled = Some(back);
}
fn word_start(&self) -> usize {
let before = self.text.get(..self.cursor).unwrap_or_default();
let trimmed = before.iter().rposition(|c| !c.is_whitespace());
trimmed.map_or(0, |last| {
before
.get(..last)
.unwrap_or_default()
.iter()
.rposition(|c| c.is_whitespace())
.map_or(0, |space| space.saturating_add(1))
})
}
fn word_end(&self) -> usize {
let after = self.text.get(self.cursor..).unwrap_or_default();
let first = after.iter().position(|c| !c.is_whitespace());
first.map_or(self.text.len(), |first| {
let rest = after.get(first..).unwrap_or_default();
let length = rest
.iter()
.position(|c| c.is_whitespace())
.unwrap_or(rest.len());
self.cursor.saturating_add(first).saturating_add(length)
})
}
}
pub(crate) fn columns(ch: char) -> usize {
match u32::from(ch) {
0x1100..=0x115F
| 0x2E80..=0x303E
| 0x3041..=0x33FF
| 0x3400..=0x4DBF
| 0x4E00..=0x9FFF
| 0xA000..=0xA4CF
| 0xAC00..=0xD7A3
| 0xF900..=0xFAFF
| 0xFE30..=0xFE4F
| 0xFF00..=0xFF60
| 0xFFE0..=0xFFE6
| 0x1F300..=0x1F64F
| 0x1F900..=0x1F9FF
| 0x20000..=0x3FFFD => 2,
_ => 1,
}
}