use std::fmt::{Display, Write as _};
use std::io::{self, BufRead, BufReader, ErrorKind, Read, Seek, Write};
use std::iter::{self, repeat, successors as scsr};
use std::{fs::File, path::Path, process::Command, time::Instant};
use crate::row::{HlState, Row};
use crate::{Config, Error, ansi_escape::*, syntax::Conf as SyntaxConf, sys, terminal};
const fn ctrl_key(key: u8) -> u8 { key & 0x1f }
const EXIT: u8 = ctrl_key(b'Q');
const DELETE_BIS: u8 = ctrl_key(b'H');
const REFRESH_SCREEN: u8 = ctrl_key(b'L');
const SAVE: u8 = ctrl_key(b'S');
const FIND: u8 = ctrl_key(b'F');
const GOTO: u8 = ctrl_key(b'G');
const CUT: u8 = ctrl_key(b'X');
const COPY: u8 = ctrl_key(b'C');
const PASTE: u8 = ctrl_key(b'V');
const DUPLICATE: u8 = ctrl_key(b'D');
const EXECUTE: u8 = ctrl_key(b'E');
const REMOVE_LINE: u8 = ctrl_key(b'R');
const TOGGLE_COMMENT: u8 = 31;
const BACKSPACE: u8 = 127;
const WELCOME_MESSAGE: &str = concat!("Kibi ", env!("CARGO_PKG_VERSION"));
const HELP_MESSAGE: &str = "^S save | ^Q quit | ^F find | ^G go to | ^D duplicate | ^E execute | \
^C copy | ^X cut | ^V paste | ^/ comment";
macro_rules! set_status { ($editor:expr, $($arg:expr),*) => ($editor.status_msg = Some(StatusMessage::new(format!($($arg),*)))) }
#[cfg_attr(test, derive(Debug, PartialEq))]
enum Key {
Arrow(AKey),
CtrlArrow(AKey),
PageUp,
PageDown,
Home,
End,
Delete,
Escape,
Char(u8),
}
#[cfg_attr(test, derive(Debug, PartialEq))]
enum AKey {
Left,
Right,
Up,
Down,
}
#[derive(Debug, Default, Clone, PartialEq)]
struct CursorState {
x: usize,
y: usize,
roff: usize,
coff: usize,
}
impl CursorState {
const fn move_to_next_line(&mut self) { (self.x, self.y) = (0, self.y + 1); }
fn scroll(&mut self, rx: usize, screen_rows: usize, screen_cols: usize) {
self.roff = self.roff.clamp(self.y.saturating_sub(screen_rows.saturating_sub(1)), self.y);
self.coff = self.coff.clamp(rx.saturating_sub(screen_cols.saturating_sub(1)), rx);
}
}
#[derive(Default)]
pub struct Editor {
prompt_mode: Option<PromptMode>,
cursor: CursorState,
ln_pad: usize,
window_width: usize,
screen_rows: usize,
screen_cols: usize,
rows: Vec<Row>,
dirty: bool,
config: Config,
quit_times: usize,
file_name: Option<String>,
status_msg: Option<StatusMessage>,
syntax: SyntaxConf,
n_bytes: u64,
copied_row: Vec<u8>,
use_color: bool,
}
struct StatusMessage {
msg: String,
time: Instant,
}
impl StatusMessage {
fn new(msg: String) -> Self { Self { msg, time: Instant::now() } }
}
fn format_size(n: u64) -> String {
if n < 1024 {
return format!("{n}B");
}
let i = n.ilog2() / 10;
let q = 100 * n / (1024 << ((i - 1) * 10));
format!("{}.{:02}{}B", q / 100, q % 100, b" kMGTPEZ"[i as usize] as char)
}
fn get_akey(c: u8) -> AKey {
match c {
b'a' | b'A' => AKey::Up,
b'b' | b'B' => AKey::Down,
b'c' | b'C' => AKey::Right,
b'd' | b'D' => AKey::Left,
_ => unreachable!("Invalid ANSI code for arrow key {}", c),
}
}
impl Editor {
fn current_row(&self) -> Option<&Row> { self.rows.get(self.cursor.y) }
fn rx(&self) -> usize { self.current_row().map_or(0, |r| r.cx2rx[self.cursor.x]) }
fn move_cursor(&mut self, key: &AKey, ctrl: bool) {
match (key, self.current_row()) {
(AKey::Left, Some(row)) if self.cursor.x > 0 => {
let mut cursor_x = self.cursor.x - row.get_char_size(row.cx2rx[self.cursor.x] - 1);
while ctrl && cursor_x > 0 && row.chars[cursor_x - 1] != b' ' {
cursor_x -= row.get_char_size(row.cx2rx[cursor_x] - 1);
}
self.cursor.x = cursor_x;
}
(AKey::Left, _) if self.cursor.y > 0 =>
(self.cursor.y, self.cursor.x) = (self.cursor.y - 1, usize::MAX),
(AKey::Right, Some(row)) if self.cursor.x < row.chars.len() => {
let mut cursor_x = self.cursor.x + row.get_char_size(row.cx2rx[self.cursor.x]);
while ctrl && cursor_x < row.chars.len() && row.chars[cursor_x] != b' ' {
cursor_x += row.get_char_size(row.cx2rx[cursor_x]);
}
self.cursor.x = cursor_x;
}
(AKey::Right, Some(_)) => self.cursor.move_to_next_line(),
(AKey::Up, _) if self.cursor.y > 0 => self.cursor.y -= 1,
(AKey::Down, Some(_)) => self.cursor.y += 1,
_ => (),
}
self.update_cursor_x_position();
}
fn update_cursor_x_position(&mut self) {
self.cursor.x = self.cursor.x.min(self.current_row().map_or(0, |row| row.chars.len()));
}
fn loop_until_keypress(&mut self, input: &mut impl BufRead) -> Result<Key, Error> {
let mut bytes = input.bytes();
loop {
if sys::has_window_size_changed() {
self.update_window_size()?;
self.refresh_screen()?;
}
if let Some(a) = bytes.next().transpose()? {
if a != b'\x1b' {
return Ok(Key::Char(a));
}
return Ok(match bytes.next().transpose()? {
Some(b @ (b'[' | b'O')) => match (b, bytes.next().transpose()?) {
(b'[', Some(c @ b'A'..=b'D')) => Key::Arrow(get_akey(c)),
(b'[' | b'O', Some(b'H')) => Key::Home,
(b'[' | b'O', Some(b'F')) => Key::End,
(b'[', mut c @ Some(b'0'..=b'8')) => {
let mut d = bytes.next().transpose()?;
if (c, d) == (Some(b'1'), Some(b';')) {
c = bytes.next().transpose()?;
d = bytes.next().transpose()?;
}
match (c, d) {
(Some(c), Some(b'~')) if c == b'1' || c == b'7' => Key::Home,
(Some(c), Some(b'~')) if c == b'4' || c == b'8' => Key::End,
(Some(b'3'), Some(b'~')) => Key::Delete,
(Some(b'5'), Some(b'~')) => Key::PageUp,
(Some(b'6'), Some(b'~')) => Key::PageDown,
(Some(b'5'), Some(d @ b'A'..=b'D')) => Key::CtrlArrow(get_akey(d)),
_ => Key::Escape,
}
}
(b'O', Some(c @ b'a'..=b'd')) => Key::CtrlArrow(get_akey(c)),
_ => Key::Escape,
},
_ => Key::Escape,
});
}
}
}
fn update_window_size(&mut self) -> Result<(), Error> {
let wsize = sys::get_window_size().or_else(|_| terminal::get_window_size_using_cursor())?;
(self.screen_rows, self.window_width) = (wsize.0.saturating_sub(2), wsize.1);
self.update_screen_cols();
Ok(())
}
fn update_screen_cols(&mut self) {
let n_digits = scsr(Some(self.rows.len()), |u| Some(u / 10).filter(|u| *u > 0)).count();
let show_line_num = self.config.show_line_num && n_digits + 2 < self.window_width / 4;
self.ln_pad = if show_line_num { n_digits + 2 } else { 0 };
self.screen_cols = self.window_width.saturating_sub(self.ln_pad);
}
fn update_row(&mut self, y: usize, ignore_following_rows: bool) {
let mut hl_state = if y > 0 { self.rows[y - 1].hl_state } else { HlState::Normal };
for row in self.rows.iter_mut().skip(y) {
let previous_hl_state = row.hl_state;
hl_state = row.update(&self.syntax, hl_state, self.config.tab_stop);
if ignore_following_rows || hl_state == previous_hl_state {
return;
}
}
}
fn update_all_rows(&mut self) {
let mut hl_state = HlState::Normal;
for row in &mut self.rows {
hl_state = row.update(&self.syntax, hl_state, self.config.tab_stop);
}
}
fn insert_byte(&mut self, c: u8) {
if let Some(row) = self.rows.get_mut(self.cursor.y) {
row.chars.insert(self.cursor.x, c);
} else {
self.rows.push(Row::new(vec![c]));
self.update_screen_cols();
}
self.update_row(self.cursor.y, false);
(self.cursor.x, self.n_bytes, self.dirty) = (self.cursor.x + 1, self.n_bytes + 1, true);
}
fn insert_new_line(&mut self) {
let (position, new_row_chars) = if self.cursor.x == 0 {
(self.cursor.y, Vec::new())
} else {
let new_chars = self.rows[self.cursor.y].chars.split_off(self.cursor.x);
self.update_row(self.cursor.y, false);
(self.cursor.y + 1, new_chars)
};
self.rows.insert(position, Row::new(new_row_chars));
self.update_row(position, false);
self.update_screen_cols();
self.cursor.move_to_next_line();
self.dirty = true;
}
fn delete_char(&mut self) {
if self.cursor.x > 0 {
let row = &mut self.rows[self.cursor.y];
let n_bytes_to_remove = row.get_char_size(row.cx2rx[self.cursor.x] - 1);
row.chars.splice(self.cursor.x - n_bytes_to_remove..self.cursor.x, iter::empty());
self.update_row(self.cursor.y, false);
self.cursor.x -= n_bytes_to_remove;
self.dirty = if self.is_empty() { self.file_name.is_some() } else { true };
self.n_bytes -= n_bytes_to_remove as u64;
} else if self.cursor.y < self.rows.len() && self.cursor.y > 0 {
let row = self.rows.remove(self.cursor.y);
let previous_row = &mut self.rows[self.cursor.y - 1];
self.cursor.x = previous_row.chars.len();
previous_row.chars.extend(&row.chars);
self.update_row(self.cursor.y - 1, true);
self.update_row(self.cursor.y, false);
self.update_screen_cols();
(self.dirty, self.cursor.y) = (true, self.cursor.y - 1);
} else if self.cursor.y == self.rows.len() {
self.move_cursor(&AKey::Left, false);
}
}
fn delete_current_row(&mut self) {
if self.cursor.y < self.rows.len() {
self.rows[self.cursor.y].chars.clear();
self.cursor.x = 0;
self.cursor.y = std::cmp::min(self.cursor.y + 1, self.rows.len() - 1);
self.delete_char();
self.cursor.x = 0;
}
}
fn duplicate_current_row(&mut self) {
self.copy_current_row();
self.paste_current_row();
}
fn copy_current_row(&mut self) {
if let Some(row) = self.current_row() {
self.copied_row = row.chars.clone();
}
}
fn paste_current_row(&mut self) {
if self.copied_row.is_empty() {
return;
}
self.n_bytes += self.copied_row.len() as u64;
let y = (self.cursor.y + 1).min(self.rows.len());
self.rows.insert(y, Row::new(self.copied_row.clone()));
self.update_row(y, false);
(self.cursor.y, self.dirty) = (y, true);
self.update_screen_cols();
}
fn toggle_comment(&mut self) {
let Some(sym) = self.syntax.sl_comment_start.first() else { return };
let Some(row) = self.rows.get_mut(self.cursor.y) else { return };
let pos = row.chars.iter().position(|&c| !(c as char).is_whitespace()).unwrap_or(0);
let n_update = if row.chars.get(pos..pos + sym.len()) == Some(sym.as_bytes()) {
let to_remove = sym.len() + usize::from(row.chars.get(pos + sym.len()) == Some(&b' '));
0isize.saturating_sub_unsigned(row.chars.drain(pos..pos + to_remove).len())
} else {
row.chars.splice(pos..pos, iter::chain(sym.bytes(), iter::once(b' ')));
1isize.saturating_add_unsigned(sym.len())
};
self.n_bytes = self.n_bytes.saturating_add_signed(n_update as i64);
if self.cursor.x >= pos {
self.cursor.x = self.cursor.x.saturating_add_signed(n_update);
}
self.update_row(self.cursor.y, false);
self.update_cursor_x_position();
self.dirty = true;
}
fn load(&mut self, path: &Path) -> Result<(), Error> {
let mut file = match File::open(path) {
Err(e) if e.kind() == ErrorKind::NotFound => {
self.rows.push(Row::new(Vec::new()));
return Ok(());
}
r => r,
}?;
let ft = file.metadata()?.file_type();
if !(ft.is_file() || ft.is_symlink()) {
return Err(io::Error::new(ErrorKind::InvalidInput, "Invalid input file type").into());
}
for line in BufReader::new(&file).split(b'\n') {
self.rows.push(Row::new(line?));
}
file.seek(io::SeekFrom::End(0))?;
#[expect(clippy::unbuffered_bytes)]
if file.bytes().next().transpose()?.is_none_or(|b| b == b'\n') {
self.rows.push(Row::new(Vec::new()));
}
self.update_all_rows();
self.update_screen_cols();
self.n_bytes = self.rows.iter().map(|row| row.chars.len() as u64).sum();
Ok(())
}
fn save(&self, file_name: &str) -> Result<usize, io::Error> {
let mut file = File::create(file_name)?;
let mut written = 0;
for (i, row) in self.rows.iter().enumerate() {
file.write_all(&row.chars)?;
written += row.chars.len();
if i != (self.rows.len() - 1) {
file.write_all(b"\n")?;
written += 1;
}
}
file.sync_all()?;
Ok(written)
}
fn save_and_handle_io_errors(&mut self, file_name: &str) -> bool {
let saved = self.save(file_name);
match saved.as_ref() {
Ok(w) => set_status!(self, "{} written to {}", format_size(*w as u64), file_name),
Err(err) => set_status!(self, "Can't save! I/O error: {err}"),
}
self.dirty &= saved.is_err();
saved.is_ok()
}
fn save_as(&mut self, file_name: String) {
if self.save_and_handle_io_errors(&file_name) {
self.syntax = SyntaxConf::find(&file_name, &sys::data_dirs());
self.file_name = Some(file_name);
self.update_all_rows();
}
}
fn draw_left_padding<T: Display>(&self, buffer: &mut String, val: T) {
if self.ln_pad >= 2 {
let s = format!("{:>1$} \u{2502}", val, self.ln_pad - 2);
push_colored(buffer, "\x1b[38;5;240m", &s, self.use_color);
}
}
const fn is_empty(&self) -> bool { self.rows.len() <= 1 && self.n_bytes == 0 }
fn draw_rows(&self, buffer: &mut String) -> Result<(), Error> {
let row_it = self.rows.iter().map(Some).chain(repeat(None)).enumerate();
for (i, row) in row_it.skip(self.cursor.roff).take(self.screen_rows) {
buffer.push_str(CLEAR_LINE_RIGHT_OF_CURSOR);
if let Some(row) = row {
self.draw_left_padding(buffer, i + 1);
row.draw(self.cursor.coff, self.screen_cols, buffer, self.use_color);
} else {
self.draw_left_padding(buffer, '~');
if self.is_empty() && i == self.screen_rows / 3 {
write!(buffer, "{:^1$.1$}", WELCOME_MESSAGE, self.screen_cols)?;
}
}
buffer.push_str("\r\n");
}
Ok(())
}
fn draw_status_bar(&self, buffer: &mut String) {
let modified = if self.dirty { " (modified)" } else { "" };
let mut left =
format!("{:.30}{modified}", self.file_name.as_deref().unwrap_or("[No Name]"));
left.truncate(self.window_width);
let size = format_size(self.n_bytes + self.rows.len().saturating_sub(1) as u64);
let right =
format!("{} | {size} | {}:{}", self.syntax.name, self.cursor.y + 1, self.rx() + 1);
let rw = self.window_width.saturating_sub(left.len());
push_colored(buffer, WBG, &format!("{left}{right:>rw$.rw$}\r\n"), self.use_color);
}
fn draw_message_bar(&self, buffer: &mut String) {
buffer.push_str(CLEAR_LINE_RIGHT_OF_CURSOR);
let msg_duration = self.config.message_dur;
if let Some(sm) = self.status_msg.as_ref().filter(|sm| sm.time.elapsed() < msg_duration) {
buffer.push_str(&sm.msg[..sm.msg.len().min(self.window_width)]);
}
}
fn refresh_screen(&mut self) -> Result<(), Error> {
self.cursor.scroll(self.rx(), self.screen_rows, self.screen_cols);
let mut buffer = format!("{HIDE_CURSOR}{MOVE_CURSOR_TO_START}");
self.draw_rows(&mut buffer)?;
self.draw_status_bar(&mut buffer);
self.draw_message_bar(&mut buffer);
let (cursor_x, cursor_y) = if self.prompt_mode.is_none() {
(self.rx() - self.cursor.coff + 1 + self.ln_pad, self.cursor.y - self.cursor.roff + 1)
} else {
(self.status_msg.as_ref().map_or(0, |sm| sm.msg.len() + 1), self.screen_rows + 2)
};
print!("{buffer}\x1b[{cursor_y};{cursor_x}H{SHOW_CURSOR}");
io::stdout().flush().map_err(Error::from)
}
fn process_keypress(&mut self, key: &Key) -> (bool, Option<PromptMode>) {
let mut reset_quit_times = true;
let mut prompt_mode = None;
match key {
Key::Arrow(arrow) => self.move_cursor(arrow, false),
Key::CtrlArrow(arrow) => self.move_cursor(arrow, true),
Key::PageUp => {
self.cursor.y = self.cursor.roff.saturating_sub(self.screen_rows);
self.update_cursor_x_position();
}
Key::PageDown => {
self.cursor.y = (self.cursor.roff + 2 * self.screen_rows - 1).min(self.rows.len());
self.update_cursor_x_position();
}
Key::Home => self.cursor.x = 0,
Key::End => self.cursor.x = self.current_row().map_or(0, |row| row.chars.len()),
Key::Char(b'\r' | b'\n') => self.insert_new_line(), Key::Char(BACKSPACE | DELETE_BIS) => self.delete_char(), Key::Char(REMOVE_LINE) => self.delete_current_row(),
Key::Delete => {
self.move_cursor(&AKey::Right, false);
self.delete_char();
}
Key::Escape | Key::Char(REFRESH_SCREEN) => (),
Key::Char(EXIT) => {
if !self.dirty || self.quit_times + 1 >= self.config.quit_times {
return (true, None);
}
let r = self.config.quit_times - self.quit_times - 1;
set_status!(self, "Press Ctrl+Q {0} more time{1:.2$} to quit.", r, "s", r - 1);
reset_quit_times = false;
}
Key::Char(SAVE) => match self.file_name.take() {
Some(file_name) => {
self.save_and_handle_io_errors(&file_name);
self.file_name = Some(file_name);
}
None => prompt_mode = Some(PromptMode::Save(String::new())),
},
Key::Char(FIND) =>
prompt_mode = Some(PromptMode::Find(String::new(), self.cursor.clone(), None)),
Key::Char(GOTO) => prompt_mode = Some(PromptMode::GoTo(String::new())),
Key::Char(DUPLICATE) => self.duplicate_current_row(),
Key::Char(CUT) => {
self.copy_current_row();
self.delete_current_row();
}
Key::Char(COPY) => self.copy_current_row(),
Key::Char(PASTE) => self.paste_current_row(),
Key::Char(TOGGLE_COMMENT) => self.toggle_comment(),
Key::Char(EXECUTE) => prompt_mode = Some(PromptMode::Execute(String::new())),
Key::Char(c) => self.insert_byte(*c),
}
self.quit_times = if reset_quit_times { 0 } else { self.quit_times + 1 };
(false, prompt_mode)
}
fn find(&mut self, query: &str, last_match: Option<usize>, forward: bool) -> Option<usize> {
let num_rows = if query.is_empty() { 0 } else { self.rows.len() };
let mut current = last_match.unwrap_or_else(|| num_rows.saturating_sub(1));
for _ in 0..num_rows {
current = (current + if forward { 1 } else { num_rows - 1 }) % num_rows;
let row = &mut self.rows[current];
if let Some(cx) = row.chars.windows(query.len()).position(|w| w == query.as_bytes()) {
(self.cursor.x, self.cursor.y, self.cursor.coff) = (cx, current, 0);
let rx = row.cx2rx[cx];
row.match_segment = Some(rx..rx + query.len());
return Some(current);
}
}
None
}
pub fn run<I: BufRead>(&mut self, file_name: Option<&str>, input: &mut I) -> Result<(), Error> {
self.update_window_size()?;
set_status!(self, "{HELP_MESSAGE}");
if let Some(path) = file_name.map(sys::path) {
self.syntax = SyntaxConf::find(&path.to_string_lossy(), &sys::data_dirs());
self.load(path.as_path())?;
self.file_name = Some(path.to_string_lossy().to_string());
} else {
self.rows.push(Row::new(Vec::new()));
self.file_name = None;
}
loop {
if let Some(mode) = &self.prompt_mode {
set_status!(self, "{}", mode.status_msg());
}
self.refresh_screen()?;
let key = self.loop_until_keypress(input)?;
self.prompt_mode = match self.prompt_mode.take() {
None => match self.process_keypress(&key) {
(true, _) => return Ok(()),
(false, prompt_mode) => prompt_mode,
},
Some(prompt_mode) => prompt_mode.process_keypress(self, &key),
}
}
}
}
pub fn run<I: BufRead>(file_name: Option<&str>, input: &mut I) -> Result<(), Error> {
sys::register_winsize_change_signal_handler()?;
let orig_term_mode = sys::enable_raw_mode()?;
let mut editor = Editor { config: Config::load(), ..Default::default() };
editor.use_color = !std::env::var("NO_COLOR").is_ok_and(|val| !val.is_empty());
print!("{USE_ALTERNATE_SCREEN}");
let prev_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
terminal::restore_terminal(&orig_term_mode).unwrap_or_else(|e| eprintln!("{e}"));
prev_hook(info);
}));
let result = editor.run(file_name, input);
terminal::restore_terminal(&orig_term_mode)?;
result
}
#[cfg_attr(test, derive(Debug, PartialEq))]
enum PromptMode {
Save(String),
Find(String, CursorState, Option<usize>),
GoTo(String),
Execute(String),
}
impl PromptMode {
fn status_msg(&self) -> String {
match self {
Self::Save(buffer) => format!("Save as: {buffer}"),
Self::Find(buffer, ..) => format!("Search (Use ESC/Arrows/Enter): {buffer}"),
Self::GoTo(buffer) => format!("Enter line number[:column number]: {buffer}"),
Self::Execute(buffer) => format!("Command to execute: {buffer}"),
}
}
fn process_keypress(self, ed: &mut Editor, key: &Key) -> Option<Self> {
ed.status_msg = None;
match self {
Self::Save(b) => match process_prompt_keypress(b, key) {
PromptState::Active(b) => return Some(Self::Save(b)),
PromptState::Cancelled => set_status!(ed, "Save aborted"),
PromptState::Completed(file_name) => ed.save_as(file_name),
},
Self::Find(b, saved_cursor, last_match) => {
if let Some(row_idx) = last_match {
ed.rows[row_idx].match_segment = None;
}
match process_prompt_keypress(b, key) {
PromptState::Active(query) => {
#[expect(clippy::wildcard_enum_match_arm)]
let (last_match, forward) = match key {
Key::Arrow(AKey::Right | AKey::Down) | Key::Char(FIND) =>
(last_match, true),
Key::Arrow(AKey::Left | AKey::Up) => (last_match, false),
_ => (None, true),
};
let curr_match = ed.find(&query, last_match, forward);
return Some(Self::Find(query, saved_cursor, curr_match));
}
PromptState::Cancelled => ed.cursor = saved_cursor,
PromptState::Completed(_) => (),
}
}
Self::GoTo(b) => match process_prompt_keypress(b, key) {
PromptState::Active(b) => return Some(Self::GoTo(b)),
PromptState::Cancelled => (),
PromptState::Completed(b) => {
let mut split = b.splitn(2, ':')
.map(|u| u.trim().parse().map(|s: usize| s.saturating_sub(1)));
match (split.next().transpose(), split.next().transpose()) {
(Ok(Some(y)), Ok(x)) => {
ed.cursor.y = y.min(ed.rows.len());
if let Some(rx) = x {
ed.cursor.x = ed.current_row().map_or(0, |r| r.rx2cx[rx]);
} else {
ed.update_cursor_x_position();
}
}
(Err(e), _) | (_, Err(e)) => set_status!(ed, "Parsing error: {e}"),
(Ok(None), _) => (),
}
}
},
Self::Execute(b) => match process_prompt_keypress(b, key) {
PromptState::Active(b) => return Some(Self::Execute(b)),
PromptState::Cancelled => (),
PromptState::Completed(b) => {
let mut args = b.split_whitespace();
match Command::new(args.next().unwrap_or_default()).args(args).output() {
Ok(out) if !out.status.success() =>
set_status!(ed, "{}", String::from_utf8_lossy(&out.stderr).trim_end()),
Ok(out) => out.stdout.into_iter().for_each(|c| match c {
b'\n' => ed.insert_new_line(),
c => ed.insert_byte(c),
}),
Err(e) => set_status!(ed, "{e}"),
}
}
},
}
None
}
}
#[cfg_attr(test, derive(Debug, PartialEq))]
enum PromptState {
Active(String),
Completed(String),
Cancelled,
}
fn process_prompt_keypress(mut buffer: String, key: &Key) -> PromptState {
#[expect(clippy::wildcard_enum_match_arm)]
match key {
Key::Char(b'\r') => return PromptState::Completed(buffer),
Key::Escape | Key::Char(EXIT) => return PromptState::Cancelled,
Key::Char(BACKSPACE | DELETE_BIS) => _ = buffer.pop(),
Key::Char(c @ 0..=126) if !c.is_ascii_control() => buffer.push(*c as char),
_ => (),
}
PromptState::Active(buffer)
}
#[cfg(test)]
mod tests {
use std::io::Cursor;
use rstest::rstest;
use super::*;
use crate::syntax::HlType;
fn assert_row_chars_equal(editor: &Editor, expected: &[&[u8]]) {
assert_eq!(
editor.rows.len(),
expected.len(),
"editor has {} rows, expected {}",
editor.rows.len(),
expected.len()
);
for (i, (row, expected)) in editor.rows.iter().zip(expected).enumerate() {
assert_eq!(
row.chars,
*expected,
"comparing characters for row {}\n left: {}\n right: {}",
i,
String::from_utf8_lossy(&row.chars),
String::from_utf8_lossy(expected)
);
}
}
fn assert_row_synthax_highlighting_types_equal(editor: &Editor, expected: &[&[HlType]]) {
assert_eq!(
editor.rows.len(),
expected.len(),
"editor has {} rows, expected {}",
editor.rows.len(),
expected.len()
);
for (i, (row, expected)) in editor.rows.iter().zip(expected).enumerate() {
assert_eq!(row.hl, *expected, "comparing HlTypes for row {i}",);
}
}
#[rstest]
#[case(0, "0B")]
#[case(1, "1B")]
#[case(1023, "1023B")]
#[case(1024, "1.00kB")]
#[case(1536, "1.50kB")]
#[case(21 * 1024 - 11, "20.98kB")]
#[case(21 * 1024 - 10, "20.99kB")]
#[case(21 * 1024 - 3, "20.99kB")]
#[case(21 * 1024, "21.00kB")]
#[case(21 * 1024 + 3, "21.00kB")]
#[case(21 * 1024 + 10, "21.00kB")]
#[case(21 * 1024 + 11, "21.01kB")]
#[case(1024 * 1024 - 1, "1023.99kB")]
#[case(1024 * 1024, "1.00MB")]
#[case(1024 * 1024 + 1, "1.00MB")]
#[case(100 * 1024 * 1024 * 1024, "100.00GB")]
#[case(313 * 1024 * 1024 * 1024 * 1024, "313.00TB")]
fn format_size_output(#[case] input: u64, #[case] expected_output: &str) {
assert_eq!(format_size(input), expected_output);
}
#[test]
fn editor_insert_byte() {
let mut editor = Editor::default();
let editor_cursor_x_before = editor.cursor.x;
editor.insert_byte(b'X');
editor.insert_byte(b'Y');
editor.insert_byte(b'Z');
assert_eq!(editor.cursor.x, editor_cursor_x_before + 3);
assert_eq!(editor.rows.len(), 1);
assert_eq!(editor.n_bytes, 3);
assert_eq!(editor.rows[0].chars, [b'X', b'Y', b'Z']);
}
#[test]
fn editor_insert_new_line() {
let mut editor = Editor::default();
let editor_cursor_y_before = editor.cursor.y;
for _ in 0..3 {
editor.insert_new_line();
}
assert_eq!(editor.cursor.y, editor_cursor_y_before + 3);
assert_eq!(editor.rows.len(), 3);
assert_eq!(editor.n_bytes, 0);
for row in &editor.rows {
assert_eq!(row.chars, []);
}
}
#[test]
fn editor_delete_char() {
let mut editor = Editor::default();
for b in b"Hello world!" {
editor.insert_byte(*b);
}
editor.delete_char();
assert_row_chars_equal(&editor, &[b"Hello world"]);
editor.move_cursor(&AKey::Left, true);
editor.move_cursor(&AKey::Left, false);
editor.move_cursor(&AKey::Left, false);
editor.delete_char();
assert_row_chars_equal(&editor, &[b"Helo world"]);
}
#[test]
fn editor_delete_next_char() {
let mut editor = Editor::default();
for &b in b"Hello world!\nHappy New Year!" {
editor.process_keypress(&Key::Char(b));
}
editor.process_keypress(&Key::Delete);
assert_row_chars_equal(&editor, &[b"Hello world!", b"Happy New Year!"]);
editor.move_cursor(&AKey::Left, true);
editor.process_keypress(&Key::Delete);
assert_row_chars_equal(&editor, &[b"Hello world!", b"Happy New ear!"]);
editor.move_cursor(&AKey::Left, true);
editor.move_cursor(&AKey::Left, true);
editor.move_cursor(&AKey::Left, true);
editor.process_keypress(&Key::Delete);
assert_row_chars_equal(&editor, &[b"Hello world!Happy New ear!"]);
}
#[test]
fn editor_move_cursor_left() {
let mut editor = Editor::default();
for &b in b"Hello world!\nHappy New Year!" {
editor.process_keypress(&Key::Char(b));
}
assert_eq!(editor.cursor.x, 15);
assert_eq!(editor.cursor.y, 1);
editor.move_cursor(&AKey::Left, true);
assert_eq!(editor.cursor.x, 10);
assert_eq!(editor.cursor.y, 1);
editor.move_cursor(&AKey::Left, false);
assert_eq!(editor.cursor.x, 9);
assert_eq!(editor.cursor.y, 1);
editor.move_cursor(&AKey::Left, true);
assert_eq!(editor.cursor.x, 6);
assert_eq!(editor.cursor.y, 1);
editor.move_cursor(&AKey::Left, true);
assert_eq!(editor.cursor.x, 0);
assert_eq!(editor.cursor.y, 1);
editor.move_cursor(&AKey::Left, false);
assert_eq!(editor.cursor.x, 12);
assert_eq!(editor.cursor.y, 0);
editor.move_cursor(&AKey::Left, true);
assert_eq!(editor.cursor.x, 6);
assert_eq!(editor.cursor.y, 0);
editor.move_cursor(&AKey::Left, true);
assert_eq!(editor.cursor.x, 0);
assert_eq!(editor.cursor.y, 0);
editor.move_cursor(&AKey::Left, false);
assert_eq!(editor.cursor.x, 0);
assert_eq!(editor.cursor.y, 0);
}
#[test]
fn editor_move_cursor_up() {
let mut editor = Editor::default();
for &b in b"abcdefgh\nij\nklmnopqrstuvwxyz" {
editor.process_keypress(&Key::Char(b));
}
assert_eq!(editor.cursor.x, 16);
assert_eq!(editor.cursor.y, 2);
editor.move_cursor(&AKey::Up, false);
assert_eq!(editor.cursor.x, 2);
assert_eq!(editor.cursor.y, 1);
editor.move_cursor(&AKey::Up, true);
assert_eq!(editor.cursor.x, 2);
assert_eq!(editor.cursor.y, 0);
editor.move_cursor(&AKey::Up, false);
assert_eq!(editor.cursor.x, 2);
assert_eq!(editor.cursor.y, 0);
}
#[test]
fn editor_move_cursor_right() {
let mut editor = Editor::default();
for &b in b"Hello world\nHappy New Year" {
editor.process_keypress(&Key::Char(b));
}
assert_eq!(editor.cursor.x, 14);
assert_eq!(editor.cursor.y, 1);
editor.move_cursor(&AKey::Right, false);
assert_eq!(editor.cursor.x, 0);
assert_eq!(editor.cursor.y, 2);
editor.move_cursor(&AKey::Right, false);
assert_eq!(editor.cursor.x, 0);
assert_eq!(editor.cursor.y, 2);
editor.move_cursor(&AKey::Up, true);
editor.move_cursor(&AKey::Up, true);
assert_eq!(editor.cursor.x, 0);
assert_eq!(editor.cursor.y, 0);
editor.move_cursor(&AKey::Right, true);
assert_eq!(editor.cursor.x, 5);
assert_eq!(editor.cursor.y, 0);
editor.move_cursor(&AKey::Right, true);
assert_eq!(editor.cursor.x, 11);
assert_eq!(editor.cursor.y, 0);
editor.move_cursor(&AKey::Right, false);
assert_eq!(editor.cursor.x, 0);
assert_eq!(editor.cursor.y, 1);
}
#[test]
fn editor_move_cursor_down() {
let mut editor = Editor::default();
for &b in b"abcdefgh\nij\nklmnopqrstuvwxyz" {
editor.process_keypress(&Key::Char(b));
}
assert_eq!(editor.cursor.x, 16);
assert_eq!(editor.cursor.y, 2);
editor.move_cursor(&AKey::Down, false);
assert_eq!(editor.cursor.x, 0);
assert_eq!(editor.cursor.y, 3);
editor.move_cursor(&AKey::Up, false);
editor.move_cursor(&AKey::Up, false);
editor.move_cursor(&AKey::Up, false);
assert_eq!(editor.cursor.x, 0);
assert_eq!(editor.cursor.y, 0);
editor.move_cursor(&AKey::Right, true);
assert_eq!(editor.cursor.x, 8);
assert_eq!(editor.cursor.y, 0);
editor.move_cursor(&AKey::Down, true);
assert_eq!(editor.cursor.x, 2);
assert_eq!(editor.cursor.y, 1);
editor.move_cursor(&AKey::Down, true);
assert_eq!(editor.cursor.x, 2);
assert_eq!(editor.cursor.y, 2);
editor.move_cursor(&AKey::Down, true);
assert_eq!(editor.cursor.x, 0);
assert_eq!(editor.cursor.y, 3);
editor.move_cursor(&AKey::Down, false);
assert_eq!(editor.cursor.x, 0);
assert_eq!(editor.cursor.y, 3);
}
#[test]
fn editor_press_home_key() {
let mut editor = Editor::default();
for &b in b"Hello\nWorld\nand\nFerris!" {
editor.process_keypress(&Key::Char(b));
}
assert_eq!(editor.cursor.x, 7);
assert_eq!(editor.cursor.y, 3);
editor.process_keypress(&Key::Home);
assert_eq!(editor.cursor.x, 0);
assert_eq!(editor.cursor.y, 3);
editor.move_cursor(&AKey::Up, false);
editor.move_cursor(&AKey::Up, false);
editor.move_cursor(&AKey::Up, false);
assert_eq!(editor.cursor.x, 0);
assert_eq!(editor.cursor.y, 0);
editor.move_cursor(&AKey::Right, true);
assert_eq!(editor.cursor.x, 5);
assert_eq!(editor.cursor.y, 0);
editor.process_keypress(&Key::Home);
assert_eq!(editor.cursor.x, 0);
assert_eq!(editor.cursor.y, 0);
}
#[test]
fn editor_press_end_key() {
let mut editor = Editor::default();
for &b in b"Hello\nWorld\nand\nFerris!" {
editor.process_keypress(&Key::Char(b));
}
assert_eq!(editor.cursor.x, 7);
assert_eq!(editor.cursor.y, 3);
editor.process_keypress(&Key::End);
assert_eq!(editor.cursor.x, 7);
assert_eq!(editor.cursor.y, 3);
editor.move_cursor(&AKey::Up, false);
editor.move_cursor(&AKey::Up, false);
editor.move_cursor(&AKey::Up, false);
assert_eq!(editor.cursor.x, 3);
assert_eq!(editor.cursor.y, 0);
editor.process_keypress(&Key::End);
assert_eq!(editor.cursor.x, 5);
assert_eq!(editor.cursor.y, 0);
}
#[test]
fn editor_page_up_moves_cursor_to_viewport_top() {
let mut editor = Editor { screen_rows: 4, ..Default::default() };
for _ in 0..10 {
editor.insert_new_line();
}
(editor.cursor.y, editor.cursor.x) = (3, 0);
editor.insert_byte(b'a');
editor.insert_byte(b'b');
(editor.cursor.y, editor.cursor.x, editor.cursor.roff) = (9, 5, 7);
let (should_quit, prompt_mode) = editor.process_keypress(&Key::PageUp);
assert!(!should_quit);
assert!(prompt_mode.is_none());
assert_eq!(editor.cursor.y, 3);
assert_eq!(editor.cursor.x, 2);
}
#[test]
fn editor_page_down_moves_cursor_to_viewport_bottom() {
let mut editor = Editor { screen_rows: 4, ..Default::default() };
for _ in 0..12 {
editor.insert_new_line();
}
(editor.cursor.y, editor.cursor.x) = (11, 0);
editor.insert_byte(b'x');
editor.insert_byte(b'y');
editor.insert_byte(b'z');
(editor.cursor.x, editor.cursor.roff) = (6, 4);
let (should_quit, prompt_mode) = editor.process_keypress(&Key::PageDown);
assert!(!should_quit);
assert!(prompt_mode.is_none());
assert_eq!(editor.cursor.y, 11);
assert_eq!(editor.cursor.x, 3);
(editor.cursor.x, editor.cursor.roff) = (9, 11);
let (should_quit, prompt_mode_again) = editor.process_keypress(&Key::PageDown);
assert!(!should_quit);
assert!(prompt_mode_again.is_none());
assert_eq!(editor.cursor.y, editor.rows.len());
assert_eq!(editor.cursor.x, 0);
}
#[rstest]
#[case::beginning_of_first_row(b"Hello\nWorld!\n", (0, 0), &[&b"World!"[..], &b""[..]], 0)]
#[case::middle_of_first_row(b"Hello\nWorld!\n", (3, 0), &[&b"World!"[..], &b""[..]], 0)]
#[case::end_of_first_row(b"Hello\nWorld!\n", (5, 0), &[&b"World!"[..], &b""[..]], 0)]
#[case::empty_first_row(b"\nHello", (0, 0), &[&b"Hello"[..]], 0)]
#[case::beginning_of_only_row(b"Hello", (0, 0), &[&b""[..]], 0)]
#[case::middle_of_only_row(b"Hello", (3, 0), &[&b""[..]], 0)]
#[case::end_of_only_row(b"Hello", (5, 0), &[&b""[..]], 0)]
#[case::beginning_of_middle_row(b"Hello\nWorld!\n", (0, 1), &[&b"Hello"[..], &b""[..]], 1)]
#[case::middle_of_middle_row(b"Hello\nWorld!\n", (3, 1), &[&b"Hello"[..], &b""[..]], 1)]
#[case::end_of_middle_row(b"Hello\nWorld!\n", (6, 1), &[&b"Hello"[..], &b""[..]], 1)]
#[case::empty_middle_row(b"Hello\n\nWorld!", (0, 1), &[&b"Hello"[..], &b"World!"[..]], 1)]
#[case::beginning_of_last_row(b"Hello\nWorld!", (0, 1), &[&b"Hello"[..]], 0)]
#[case::middle_of_last_row(b"Hello\nWorld!", (3, 1), &[&b"Hello"[..]], 0)]
#[case::end_of_last_row(b"Hello\nWorld!", (6, 1), &[&b"Hello"[..]], 0)]
#[case::empty_last_row(b"Hello\n", (0, 1), &[&b"Hello"[..]], 0)]
#[case::after_last_row(b"Hello\nWorld!", (0, 2), &[&b"Hello"[..], &b"World!"[..]], 2)]
fn delete_current_row_updates_buffer_and_position(
#[case] initial_buffer: &[u8], #[case] cursor_position: (usize, usize),
#[case] expected_rows: &[&[u8]], #[case] expected_cursor_row: usize,
) {
let mut editor = Editor::default();
for &b in initial_buffer {
editor.process_keypress(&Key::Char(b));
}
(editor.cursor.x, editor.cursor.y) = cursor_position;
editor.delete_current_row();
assert_row_chars_equal(&editor, expected_rows);
assert_eq!(
(editor.cursor.x, editor.cursor.y),
(0, expected_cursor_row),
"cursor is at {}:{}, expected {}:0",
editor.cursor.y,
editor.cursor.x,
expected_cursor_row
);
}
#[rstest]
#[case::first_row(0)]
#[case::middle_row(5)]
#[case::last_row(9)]
fn delete_current_row_updates_screen_cols_and_ln_pad(#[case] current_row: usize) {
let mut editor = Editor { window_width: 100, ..Default::default() };
for _ in 0..10 {
editor.insert_new_line();
}
assert_eq!(editor.screen_cols, 96);
assert_eq!(editor.ln_pad, 4);
editor.cursor.y = current_row;
editor.delete_current_row();
assert_eq!(editor.screen_cols, 97);
assert_eq!(editor.ln_pad, 3);
}
#[test]
fn delete_current_row_updates_syntax_highlighting() {
let mut editor = Editor {
syntax: SyntaxConf {
ml_comment_delims: Some(("/*".to_owned(), "*/".to_owned())),
..Default::default()
},
..Default::default()
};
for &b in b"A\nb/*c\nd\ne\nf*/g\nh" {
editor.process_keypress(&Key::Char(b));
}
assert_row_chars_equal(&editor, &[b"A", b"b/*c", b"d", b"e", b"f*/g", b"h"]);
assert_row_synthax_highlighting_types_equal(&editor, &[
&[HlType::Normal],
&[HlType::Normal, HlType::MlComment, HlType::MlComment, HlType::MlComment],
&[HlType::MlComment],
&[HlType::MlComment],
&[HlType::MlComment, HlType::MlComment, HlType::MlComment, HlType::Normal],
&[HlType::Normal],
]);
(editor.cursor.x, editor.cursor.y) = (0, 4);
editor.delete_current_row();
assert_row_chars_equal(&editor, &[b"A", b"b/*c", b"d", b"e", b"h"]);
assert_row_synthax_highlighting_types_equal(&editor, &[
&[HlType::Normal],
&[HlType::Normal, HlType::MlComment, HlType::MlComment, HlType::MlComment],
&[HlType::MlComment],
&[HlType::MlComment],
&[HlType::MlComment],
]);
(editor.cursor.x, editor.cursor.y) = (0, 1);
editor.delete_current_row();
assert_row_chars_equal(&editor, &[b"A", b"d", b"e", b"h"]);
assert_row_synthax_highlighting_types_equal(&editor, &[
&[HlType::Normal],
&[HlType::Normal],
&[HlType::Normal],
&[HlType::Normal],
]);
}
#[test]
fn loop_until_keypress() -> Result<(), Error> {
let mut editor = Editor::default();
let mut fake_stdin = Cursor::new(
b"abc\x1b[A\x1b[B\x1b[C\x1b[D\x1b[H\x1bOH\x1b[F\x1bOF\x1b[1;5C\x1b[5C\x1b[99",
);
for expected_key in [
Key::Char(b'a'),
Key::Char(b'b'),
Key::Char(b'c'),
Key::Arrow(AKey::Up),
Key::Arrow(AKey::Down),
Key::Arrow(AKey::Right),
Key::Arrow(AKey::Left),
Key::Home,
Key::Home,
Key::End,
Key::End,
Key::CtrlArrow(AKey::Right),
Key::CtrlArrow(AKey::Right),
Key::Escape,
] {
assert_eq!(editor.loop_until_keypress(&mut fake_stdin)?, expected_key);
}
Ok(())
}
#[rstest]
#[case::ascii_completed(&[Key::Char(b'H'), Key::Char(b'i'), Key::Char(b'\r')], &PromptState::Completed(String::from("Hi")))]
#[case::escape(&[Key::Char(b'H'), Key::Char(b'i'), Key::Escape], &PromptState::Cancelled)]
#[case::exit(&[Key::Char(b'H'), Key::Char(b'i'), Key::Char(EXIT)], &PromptState::Cancelled)]
#[case::skip_ascii_control(&[Key::Char(b'\x0A')], &PromptState::Active(String::new()))]
#[case::unsupported_non_ascii(&[Key::Char(b'\xEF')], &PromptState::Active(String::new()))]
#[case::backspace(&[Key::Char(b'H'), Key::Char(b'i'), Key::Char(BACKSPACE), Key::Char(BACKSPACE)], &PromptState::Active(String::new()))]
#[case::delete_bis(&[Key::Char(b'H'), Key::Char(b'i'), Key::Char(DELETE_BIS), Key::Char(DELETE_BIS), Key::Char(DELETE_BIS)], &PromptState::Active(String::new()))]
fn process_prompt_keypresses(#[case] keys: &[Key], #[case] expected_final_state: &PromptState) {
let mut prompt_state = PromptState::Active(String::new());
for key in keys {
if let PromptState::Active(buffer) = prompt_state {
prompt_state = process_prompt_keypress(buffer, key);
} else {
panic!("Prompt state: {prompt_state:?} is not active")
}
}
assert_eq!(prompt_state, *expected_final_state);
}
#[rstest]
#[case(&[Key::Char(b'H'), Key::Char(b'i'), Key::Char(BACKSPACE), Key::Char(b'e'), Key::Char(b'l'), Key::Char(b'l'), Key::Char(b'o')], "Hello")]
#[case(&[Key::Char(b'H'), Key::Char(b'i'), Key::Char(BACKSPACE), Key::Char(BACKSPACE), Key::Char(BACKSPACE)], "")]
fn process_find_keypress_completed(#[case] keys: &[Key], #[case] expected_final_value: &str) {
let mut ed: Editor = Editor::default();
ed.insert_new_line();
let mut prompt_mode = Some(PromptMode::Find(String::new(), CursorState::default(), None));
for key in keys {
prompt_mode = prompt_mode
.take()
.and_then(|prompt_mode| prompt_mode.process_keypress(&mut ed, key));
}
assert_eq!(
prompt_mode,
Some(PromptMode::Find(
String::from(expected_final_value),
CursorState::default(),
None
))
);
prompt_mode = prompt_mode
.take()
.and_then(|prompt_mode| prompt_mode.process_keypress(&mut ed, &Key::Char(b'\r')));
assert_eq!(prompt_mode, None);
}
#[rstest]
#[case(100, true, 12345, "\u{1b}[38;5;240m12345 │\u{1b}[m")]
#[case(100, true, "~", "\u{1b}[38;5;240m~ │\u{1b}[m")]
#[case(10, true, 12345, "")]
#[case(10, true, "~", "")]
#[case(100, false, 12345, "12345 │")]
#[case(100, false, "~", "~ │")]
#[case(10, false, 12345, "")]
#[case(10, false, "~", "")]
fn draw_left_padding<T: Display>(
#[case] window_width: usize, #[case] use_color: bool, #[case] value: T,
#[case] expected: &'static str,
) {
let mut editor = Editor { window_width, use_color, ..Default::default() };
editor.update_screen_cols();
let mut buffer = String::new();
editor.draw_left_padding(&mut buffer, value);
assert_eq!(buffer, expected);
}
#[test]
fn editor_toggle_comment() {
let mut editor = Editor::default();
editor.syntax.sl_comment_start = vec!["#".to_owned()];
for b in b"def hello():\n print(\"Hello\")\n return True" {
if *b == b'\n' {
editor.insert_new_line();
} else {
editor.insert_byte(*b);
}
}
editor.cursor.y = 0; editor.cursor.x = 0;
editor.process_keypress(&Key::Char(TOGGLE_COMMENT));
assert_eq!(editor.rows[0].chars, b"# def hello():");
editor.process_keypress(&Key::Char(TOGGLE_COMMENT));
assert_eq!(editor.rows[0].chars, b"def hello():");
editor.cursor.y = 1; editor.cursor.x = 0;
editor.process_keypress(&Key::Char(TOGGLE_COMMENT));
assert_eq!(editor.rows[1].chars, b" # print(\"Hello\")");
editor.process_keypress(&Key::Char(TOGGLE_COMMENT));
assert_eq!(editor.rows[1].chars, b" print(\"Hello\")");
editor.cursor.y = 0; editor.cursor.x = editor.rows[0].chars.len(); editor.process_keypress(&Key::Char(TOGGLE_COMMENT)); assert_eq!(editor.rows[0].chars, b"# def hello():");
editor.cursor.x = editor.rows[0].chars.len(); editor.process_keypress(&Key::Char(TOGGLE_COMMENT)); assert_eq!(editor.rows[0].chars, b"def hello():");
assert!(editor.cursor.x <= editor.rows[0].chars.len());
}
}