use crossterm::event::{Event, KeyEvent};
use edtui::{EditorEventHandler, EditorMode, EditorState, Index2, Lines};
pub struct CommentVimEditor {
state: EditorState,
events: EditorEventHandler,
}
impl CommentVimEditor {
pub fn from_buffer(text: &str, cursor_byte: usize) -> Self {
let mut state = EditorState::new(Lines::from(text));
state.mode = EditorMode::Insert;
state.cursor = byte_to_index2(text, cursor_byte);
Self {
state,
events: EditorEventHandler::default(),
}
}
pub fn is_normal_mode(&self) -> bool {
self.state.mode == EditorMode::Normal
}
pub fn label(&self) -> &'static str {
match self.state.mode {
EditorMode::Normal => "NORMAL",
EditorMode::Insert => "INSERT",
EditorMode::Visual => "VISUAL",
_ => "NORMAL",
}
}
pub fn feed_key(&mut self, key: KeyEvent) -> (String, usize) {
self.events.on_key_event(key, &mut self.state);
self.text_and_cursor()
}
pub fn feed_paste(&mut self, text: String) -> (String, usize) {
self.events.on_event(Event::Paste(text), &mut self.state);
self.text_and_cursor()
}
pub fn text_and_cursor(&self) -> (String, usize) {
let text = self.state.lines.to_string();
let cursor = index2_to_byte(&text, self.state.cursor);
(text, cursor)
}
}
pub fn index2_to_byte(text: &str, idx: Index2) -> usize {
let mut offset = 0usize;
for (row, line) in text.split('\n').enumerate() {
if row == idx.row {
for (chars, (byte, _)) in line.char_indices().enumerate() {
if chars == idx.col {
return offset + byte;
}
}
return offset + line.len();
}
offset += line.len() + 1; }
text.len()
}
pub fn byte_to_index2(text: &str, byte: usize) -> Index2 {
let byte = byte.min(text.len());
let mut row = 0usize;
let mut line_start = 0usize;
for (i, ch) in text.char_indices() {
if i >= byte {
break;
}
if ch == '\n' {
row += 1;
line_start = i + 1;
}
}
let col = text[line_start..byte].chars().count();
Index2::new(row, col)
}
#[cfg(test)]
mod tests {
use super::*;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
fn key(c: char) -> KeyEvent {
KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE)
}
fn special(code: KeyCode) -> KeyEvent {
KeyEvent::new(code, KeyModifiers::NONE)
}
#[test]
fn insert_mode_typing_syncs_buffer() {
let mut editor = CommentVimEditor::from_buffer("", 0);
assert!(!editor.is_normal_mode());
let mut last = (String::new(), 0);
for c in "hi".chars() {
last = editor.feed_key(key(c));
}
assert_eq!(last, ("hi".to_string(), 2));
assert_eq!(editor.label(), "INSERT");
}
#[test]
fn esc_switches_to_normal_mode() {
let mut editor = CommentVimEditor::from_buffer("hello", 5);
editor.feed_key(special(KeyCode::Esc));
assert!(editor.is_normal_mode());
assert_eq!(editor.label(), "NORMAL");
}
#[test]
fn normal_mode_x_deletes_char_under_cursor() {
let mut editor = CommentVimEditor::from_buffer("abc", 0);
editor.feed_key(special(KeyCode::Esc));
let (text, _) = editor.feed_key(key('x'));
assert_eq!(text, "bc");
}
#[test]
fn normal_mode_dd_deletes_line() {
let mut editor = CommentVimEditor::from_buffer("line1\nline2", 0);
editor.feed_key(special(KeyCode::Esc));
editor.feed_key(key('d'));
let (text, _) = editor.feed_key(key('d'));
assert_eq!(text, "line2");
}
fn roundtrip(text: &str, byte: usize) {
let idx = byte_to_index2(text, byte);
assert_eq!(
index2_to_byte(text, idx),
byte,
"roundtrip failed for {text:?} @ {byte}"
);
}
#[test]
fn roundtrip_ascii_multiline() {
let text = "hello\nworld\nfoo";
for byte in 0..=text.len() {
if text.is_char_boundary(byte) {
roundtrip(text, byte);
}
}
}
#[test]
fn roundtrip_multibyte() {
let text = "héllo\n世界\n👋🏽 bye";
for byte in 0..=text.len() {
if text.is_char_boundary(byte) {
roundtrip(text, byte);
}
}
}
#[test]
fn index2_clamps_past_line_and_buffer() {
let text = "ab\ncd";
assert_eq!(index2_to_byte(text, Index2::new(0, 99)), 2);
assert_eq!(index2_to_byte(text, Index2::new(99, 0)), text.len());
}
#[test]
fn byte_to_index2_on_second_line() {
let text = "ab\ncde";
let idx = byte_to_index2(text, 4);
assert_eq!((idx.row, idx.col), (1, 1));
}
}