1use std::fmt::{Display, Write as _};
6use std::io::{self, BufRead, BufReader, ErrorKind, Read, Seek, Write};
7use std::iter::{self, repeat, successors as scsr};
8use std::{fs::File, path::Path, process::Command, time::Instant};
9
10use crate::row::{HlState, Row};
11use crate::{Config, Error, ansi_escape::*, syntax::Conf as SyntaxConf, sys, terminal};
12
13const fn ctrl_key(key: u8) -> u8 { key & 0x1f }
14const EXIT: u8 = ctrl_key(b'Q');
15const DELETE_BIS: u8 = ctrl_key(b'H');
16const REFRESH_SCREEN: u8 = ctrl_key(b'L');
17const SAVE: u8 = ctrl_key(b'S');
18const FIND: u8 = ctrl_key(b'F');
19const GOTO: u8 = ctrl_key(b'G');
20const CUT: u8 = ctrl_key(b'X');
21const COPY: u8 = ctrl_key(b'C');
22const PASTE: u8 = ctrl_key(b'V');
23const DUPLICATE: u8 = ctrl_key(b'D');
24const EXECUTE: u8 = ctrl_key(b'E');
25const REMOVE_LINE: u8 = ctrl_key(b'R');
26const TOGGLE_COMMENT: u8 = 31;
27const BACKSPACE: u8 = 127;
28
29const WELCOME_MESSAGE: &str = concat!("Kibi ", env!("CARGO_PKG_VERSION"));
30const HELP_MESSAGE: &str = "^S save | ^Q quit | ^F find | ^G go to | ^D duplicate | ^E execute | \
31 ^C copy | ^X cut | ^V paste | ^/ comment";
32
33macro_rules! set_status { ($editor:expr, $($arg:expr),*) => ($editor.status_msg = Some(StatusMessage::new(format!($($arg),*)))) }
36
37#[cfg_attr(test, derive(Debug, PartialEq))]
39enum Key {
40 Arrow(AKey),
41 CtrlArrow(AKey),
42 PageUp,
43 PageDown,
44 Home,
45 End,
46 Delete,
47 Escape,
48 Char(u8),
49}
50
51#[cfg_attr(test, derive(Debug, PartialEq))]
53enum AKey {
54 Left,
55 Right,
56 Up,
57 Down,
58}
59
60#[derive(Debug, Default, Clone, PartialEq)]
62struct CursorState {
63 x: usize,
65 y: usize,
67 roff: usize,
69 coff: usize,
71}
72
73impl CursorState {
74 const fn move_to_next_line(&mut self) { (self.x, self.y) = (0, self.y + 1); }
75
76 fn scroll(&mut self, rx: usize, screen_rows: usize, screen_cols: usize) {
80 self.roff = self.roff.clamp(self.y.saturating_sub(screen_rows.saturating_sub(1)), self.y);
81 self.coff = self.coff.clamp(rx.saturating_sub(screen_cols.saturating_sub(1)), rx);
82 }
83}
84
85#[derive(Default)]
88pub struct Editor {
89 prompt_mode: Option<PromptMode>,
92 cursor: CursorState,
94 ln_pad: usize,
96 window_width: usize,
99 screen_rows: usize,
102 screen_cols: usize,
105 rows: Vec<Row>,
108 dirty: bool,
110 config: Config,
112 quit_times: usize,
115 file_name: Option<String>,
119 status_msg: Option<StatusMessage>,
121 syntax: SyntaxConf,
123 n_bytes: u64,
125 copied_row: Vec<u8>,
127 use_color: bool,
129}
130
131struct StatusMessage {
133 msg: String,
135 time: Instant,
137}
138
139impl StatusMessage {
140 fn new(msg: String) -> Self { Self { msg, time: Instant::now() } }
142}
143
144fn format_size(n: u64) -> String {
146 if n < 1024 {
147 return format!("{n}B");
148 }
149 let i = n.ilog2() / 10;
151
152 let q = 100 * n / (1024 << ((i - 1) * 10));
155 format!("{}.{:02}{}B", q / 100, q % 100, b" kMGTPEZ"[i as usize] as char)
156}
157
158fn get_akey(c: u8) -> AKey {
163 match c {
164 b'a' | b'A' => AKey::Up,
165 b'b' | b'B' => AKey::Down,
166 b'c' | b'C' => AKey::Right,
167 b'd' | b'D' => AKey::Left,
168 _ => unreachable!("Invalid ANSI code for arrow key {}", c),
169 }
170}
171
172impl Editor {
173 fn current_row(&self) -> Option<&Row> { self.rows.get(self.cursor.y) }
176
177 fn rx(&self) -> usize { self.current_row().map_or(0, |r| r.cx2rx[self.cursor.x]) }
181
182 fn move_cursor(&mut self, key: &AKey, ctrl: bool) {
184 match (key, self.current_row()) {
185 (AKey::Left, Some(row)) if self.cursor.x > 0 => {
186 let mut cursor_x = self.cursor.x - row.get_char_size(row.cx2rx[self.cursor.x] - 1);
187 while ctrl && cursor_x > 0 && row.chars[cursor_x - 1] != b' ' {
189 cursor_x -= row.get_char_size(row.cx2rx[cursor_x] - 1);
190 }
191 self.cursor.x = cursor_x;
192 }
193 (AKey::Left, _) if self.cursor.y > 0 =>
197 (self.cursor.y, self.cursor.x) = (self.cursor.y - 1, usize::MAX),
198 (AKey::Right, Some(row)) if self.cursor.x < row.chars.len() => {
199 let mut cursor_x = self.cursor.x + row.get_char_size(row.cx2rx[self.cursor.x]);
200 while ctrl && cursor_x < row.chars.len() && row.chars[cursor_x] != b' ' {
202 cursor_x += row.get_char_size(row.cx2rx[cursor_x]);
203 }
204 self.cursor.x = cursor_x;
205 }
206 (AKey::Right, Some(_)) => self.cursor.move_to_next_line(),
207 (AKey::Up, _) if self.cursor.y > 0 => self.cursor.y -= 1,
210 (AKey::Down, Some(_)) => self.cursor.y += 1,
211 _ => (),
212 }
213 self.update_cursor_x_position();
214 }
215
216 fn update_cursor_x_position(&mut self) {
220 self.cursor.x = self.cursor.x.min(self.current_row().map_or(0, |row| row.chars.len()));
221 }
222
223 fn loop_until_keypress(&mut self, input: &mut impl BufRead) -> Result<Key, Error> {
229 let mut bytes = input.bytes();
230 loop {
231 if sys::has_window_size_changed() {
233 self.update_window_size()?;
234 self.refresh_screen()?;
235 }
236 if let Some(a) = bytes.next().transpose()? {
239 if a != b'\x1b' {
240 return Ok(Key::Char(a));
241 }
242 return Ok(match bytes.next().transpose()? {
243 Some(b @ (b'[' | b'O')) => match (b, bytes.next().transpose()?) {
244 (b'[', Some(c @ b'A'..=b'D')) => Key::Arrow(get_akey(c)),
245 (b'[' | b'O', Some(b'H')) => Key::Home,
246 (b'[' | b'O', Some(b'F')) => Key::End,
247 (b'[', mut c @ Some(b'0'..=b'8')) => {
248 let mut d = bytes.next().transpose()?;
249 if (c, d) == (Some(b'1'), Some(b';')) {
250 c = bytes.next().transpose()?;
253 d = bytes.next().transpose()?;
254 }
255 match (c, d) {
256 (Some(c), Some(b'~')) if c == b'1' || c == b'7' => Key::Home,
257 (Some(c), Some(b'~')) if c == b'4' || c == b'8' => Key::End,
258 (Some(b'3'), Some(b'~')) => Key::Delete,
259 (Some(b'5'), Some(b'~')) => Key::PageUp,
260 (Some(b'6'), Some(b'~')) => Key::PageDown,
261 (Some(b'5'), Some(d @ b'A'..=b'D')) => Key::CtrlArrow(get_akey(d)),
262 _ => Key::Escape,
263 }
264 }
265 (b'O', Some(c @ b'a'..=b'd')) => Key::CtrlArrow(get_akey(c)),
266 _ => Key::Escape,
267 },
268 _ => Key::Escape,
269 });
270 }
271 }
272 }
273
274 fn update_window_size(&mut self) -> Result<(), Error> {
277 let wsize = sys::get_window_size().or_else(|_| terminal::get_window_size_using_cursor())?;
278 (self.screen_rows, self.window_width) = (wsize.0.saturating_sub(2), wsize.1);
280 self.update_screen_cols();
281 Ok(())
282 }
283
284 fn update_screen_cols(&mut self) {
288 let n_digits = scsr(Some(self.rows.len()), |u| Some(u / 10).filter(|u| *u > 0)).count();
292 let show_line_num = self.config.show_line_num && n_digits + 2 < self.window_width / 4;
293 self.ln_pad = if show_line_num { n_digits + 2 } else { 0 };
294 self.screen_cols = self.window_width.saturating_sub(self.ln_pad);
295 }
296
297 fn update_row(&mut self, y: usize, ignore_following_rows: bool) {
301 let mut hl_state = if y > 0 { self.rows[y - 1].hl_state } else { HlState::Normal };
302 for row in self.rows.iter_mut().skip(y) {
303 let previous_hl_state = row.hl_state;
304 hl_state = row.update(&self.syntax, hl_state, self.config.tab_stop);
305 if ignore_following_rows || hl_state == previous_hl_state {
306 return;
307 }
308 }
312 }
313
314 fn update_all_rows(&mut self) {
316 let mut hl_state = HlState::Normal;
317 for row in &mut self.rows {
318 hl_state = row.update(&self.syntax, hl_state, self.config.tab_stop);
319 }
320 }
321
322 fn insert_byte(&mut self, c: u8) {
325 if let Some(row) = self.rows.get_mut(self.cursor.y) {
326 row.chars.insert(self.cursor.x, c);
327 } else {
328 self.rows.push(Row::new(vec![c]));
329 self.update_screen_cols();
331 }
332 self.update_row(self.cursor.y, false);
333 (self.cursor.x, self.n_bytes, self.dirty) = (self.cursor.x + 1, self.n_bytes + 1, true);
334 }
335
336 fn insert_new_line(&mut self) {
340 let (position, new_row_chars) = if self.cursor.x == 0 {
341 (self.cursor.y, Vec::new())
342 } else {
343 let new_chars = self.rows[self.cursor.y].chars.split_off(self.cursor.x);
346 self.update_row(self.cursor.y, false);
347 (self.cursor.y + 1, new_chars)
348 };
349 self.rows.insert(position, Row::new(new_row_chars));
350 self.update_row(position, false);
351 self.update_screen_cols();
352 self.cursor.move_to_next_line();
353 self.dirty = true;
354 }
355
356 fn delete_char(&mut self) {
361 if self.cursor.x > 0 {
362 let row = &mut self.rows[self.cursor.y];
363 let n_bytes_to_remove = row.get_char_size(row.cx2rx[self.cursor.x] - 1);
366 row.chars.splice(self.cursor.x - n_bytes_to_remove..self.cursor.x, iter::empty());
367 self.update_row(self.cursor.y, false);
368 self.cursor.x -= n_bytes_to_remove;
369 self.dirty = if self.is_empty() { self.file_name.is_some() } else { true };
370 self.n_bytes -= n_bytes_to_remove as u64;
371 } else if self.cursor.y < self.rows.len() && self.cursor.y > 0 {
372 let row = self.rows.remove(self.cursor.y);
373 let previous_row = &mut self.rows[self.cursor.y - 1];
374 self.cursor.x = previous_row.chars.len();
375 previous_row.chars.extend(&row.chars);
376 self.update_row(self.cursor.y - 1, true);
377 self.update_row(self.cursor.y, false);
378 self.update_screen_cols();
380 (self.dirty, self.cursor.y) = (true, self.cursor.y - 1);
381 } else if self.cursor.y == self.rows.len() {
382 self.move_cursor(&AKey::Left, false);
385 }
386 }
387
388 fn delete_current_row(&mut self) {
389 if self.cursor.y < self.rows.len() {
390 self.rows[self.cursor.y].chars.clear();
391 self.cursor.x = 0;
392 self.cursor.y = std::cmp::min(self.cursor.y + 1, self.rows.len() - 1);
393 self.delete_char();
394 self.cursor.x = 0;
395 }
396 }
397
398 fn duplicate_current_row(&mut self) {
399 self.copy_current_row();
400 self.paste_current_row();
401 }
402
403 fn copy_current_row(&mut self) {
404 if let Some(row) = self.current_row() {
405 self.copied_row = row.chars.clone();
406 }
407 }
408
409 fn paste_current_row(&mut self) {
410 if self.copied_row.is_empty() {
411 return;
412 }
413 self.n_bytes += self.copied_row.len() as u64;
414 let y = (self.cursor.y + 1).min(self.rows.len());
415 self.rows.insert(y, Row::new(self.copied_row.clone()));
416 self.update_row(y, false);
417 (self.cursor.y, self.dirty) = (y, true);
418 self.update_screen_cols();
419 }
420
421 fn toggle_comment(&mut self) {
425 let Some(sym) = self.syntax.sl_comment_start.first() else { return };
427 let Some(row) = self.rows.get_mut(self.cursor.y) else { return };
428 let pos = row.chars.iter().position(|&c| !(c as char).is_whitespace()).unwrap_or(0);
430
431 let n_update = if row.chars.get(pos..pos + sym.len()) == Some(sym.as_bytes()) {
433 let to_remove = sym.len() + usize::from(row.chars.get(pos + sym.len()) == Some(&b' '));
434 0isize.saturating_sub_unsigned(row.chars.drain(pos..pos + to_remove).len())
436 } else {
437 row.chars.splice(pos..pos, iter::chain(sym.bytes(), iter::once(b' ')));
439 1isize.saturating_add_unsigned(sym.len())
440 };
441 self.n_bytes = self.n_bytes.saturating_add_signed(n_update as i64);
442 if self.cursor.x >= pos {
443 self.cursor.x = self.cursor.x.saturating_add_signed(n_update);
444 }
445
446 self.update_row(self.cursor.y, false);
447 self.update_cursor_x_position();
449 self.dirty = true;
450 }
451
452 fn load(&mut self, path: &Path) -> Result<(), Error> {
455 let mut file = match File::open(path) {
456 Err(e) if e.kind() == ErrorKind::NotFound => {
457 self.rows.push(Row::new(Vec::new()));
458 return Ok(());
459 }
460 r => r,
461 }?;
462 let ft = file.metadata()?.file_type();
463 if !(ft.is_file() || ft.is_symlink()) {
464 return Err(io::Error::new(ErrorKind::InvalidInput, "Invalid input file type").into());
465 }
466 for line in BufReader::new(&file).split(b'\n') {
467 self.rows.push(Row::new(line?));
468 }
469 file.seek(io::SeekFrom::End(0))?;
473 #[expect(clippy::unbuffered_bytes)]
474 if file.bytes().next().transpose()?.is_none_or(|b| b == b'\n') {
475 self.rows.push(Row::new(Vec::new()));
476 }
477 self.update_all_rows();
478 self.update_screen_cols();
480 self.n_bytes = self.rows.iter().map(|row| row.chars.len() as u64).sum();
481 Ok(())
482 }
483
484 fn save(&self, file_name: &str) -> Result<usize, io::Error> {
486 let mut file = File::create(file_name)?;
487 let mut written = 0;
488 for (i, row) in self.rows.iter().enumerate() {
489 file.write_all(&row.chars)?;
490 written += row.chars.len();
491 if i != (self.rows.len() - 1) {
492 file.write_all(b"\n")?;
493 written += 1;
494 }
495 }
496 file.sync_all()?;
497 Ok(written)
498 }
499
500 fn save_and_handle_io_errors(&mut self, file_name: &str) -> bool {
504 let saved = self.save(file_name);
505 match saved.as_ref() {
507 Ok(w) => set_status!(self, "{} written to {}", format_size(*w as u64), file_name),
508 Err(err) => set_status!(self, "Can't save! I/O error: {err}"),
509 }
510 self.dirty &= saved.is_err();
512 saved.is_ok()
513 }
514
515 fn save_as(&mut self, file_name: String) {
519 if self.save_and_handle_io_errors(&file_name) {
520 self.syntax = SyntaxConf::find(&file_name, &sys::data_dirs());
522 self.file_name = Some(file_name);
523 self.update_all_rows();
524 }
525 }
526
527 fn draw_left_padding<T: Display>(&self, buffer: &mut String, val: T) {
529 if self.ln_pad >= 2 {
530 let s = format!("{:>1$} \u{2502}", val, self.ln_pad - 2);
532 push_colored(buffer, "\x1b[38;5;240m", &s, self.use_color);
534 }
535 }
536
537 const fn is_empty(&self) -> bool { self.rows.len() <= 1 && self.n_bytes == 0 }
541
542 fn draw_rows(&self, buffer: &mut String) -> Result<(), Error> {
545 let row_it = self.rows.iter().map(Some).chain(repeat(None)).enumerate();
546 for (i, row) in row_it.skip(self.cursor.roff).take(self.screen_rows) {
547 buffer.push_str(CLEAR_LINE_RIGHT_OF_CURSOR);
548 if let Some(row) = row {
549 self.draw_left_padding(buffer, i + 1);
551 row.draw(self.cursor.coff, self.screen_cols, buffer, self.use_color);
552 } else {
553 self.draw_left_padding(buffer, '~');
555 if self.is_empty() && i == self.screen_rows / 3 {
556 write!(buffer, "{:^1$.1$}", WELCOME_MESSAGE, self.screen_cols)?;
557 }
558 }
559 buffer.push_str("\r\n");
560 }
561 Ok(())
562 }
563
564 fn draw_status_bar(&self, buffer: &mut String) {
566 let modified = if self.dirty { " (modified)" } else { "" };
568 let mut left =
569 format!("{:.30}{modified}", self.file_name.as_deref().unwrap_or("[No Name]"));
570 left.truncate(self.window_width);
571
572 let size = format_size(self.n_bytes + self.rows.len().saturating_sub(1) as u64);
574 let right =
575 format!("{} | {size} | {}:{}", self.syntax.name, self.cursor.y + 1, self.rx() + 1);
576
577 let rw = self.window_width.saturating_sub(left.len());
579 push_colored(buffer, WBG, &format!("{left}{right:>rw$.rw$}\r\n"), self.use_color);
580 }
581
582 fn draw_message_bar(&self, buffer: &mut String) {
585 buffer.push_str(CLEAR_LINE_RIGHT_OF_CURSOR);
586 let msg_duration = self.config.message_dur;
587 if let Some(sm) = self.status_msg.as_ref().filter(|sm| sm.time.elapsed() < msg_duration) {
588 buffer.push_str(&sm.msg[..sm.msg.len().min(self.window_width)]);
589 }
590 }
591
592 fn refresh_screen(&mut self) -> Result<(), Error> {
595 self.cursor.scroll(self.rx(), self.screen_rows, self.screen_cols);
596 let mut buffer = format!("{HIDE_CURSOR}{MOVE_CURSOR_TO_START}");
597 self.draw_rows(&mut buffer)?;
598 self.draw_status_bar(&mut buffer);
599 self.draw_message_bar(&mut buffer);
600 let (cursor_x, cursor_y) = if self.prompt_mode.is_none() {
601 (self.rx() - self.cursor.coff + 1 + self.ln_pad, self.cursor.y - self.cursor.roff + 1)
604 } else {
605 (self.status_msg.as_ref().map_or(0, |sm| sm.msg.len() + 1), self.screen_rows + 2)
608 };
609 print!("{buffer}\x1b[{cursor_y};{cursor_x}H{SHOW_CURSOR}");
611 io::stdout().flush().map_err(Error::from)
612 }
613
614 fn process_keypress(&mut self, key: &Key) -> (bool, Option<PromptMode>) {
618 let mut reset_quit_times = true;
620 let mut prompt_mode = None;
621
622 match key {
623 Key::Arrow(arrow) => self.move_cursor(arrow, false),
624 Key::CtrlArrow(arrow) => self.move_cursor(arrow, true),
625 Key::PageUp => {
626 self.cursor.y = self.cursor.roff.saturating_sub(self.screen_rows);
627 self.update_cursor_x_position();
628 }
629 Key::PageDown => {
630 self.cursor.y = (self.cursor.roff + 2 * self.screen_rows - 1).min(self.rows.len());
631 self.update_cursor_x_position();
632 }
633 Key::Home => self.cursor.x = 0,
634 Key::End => self.cursor.x = self.current_row().map_or(0, |row| row.chars.len()),
635 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(),
638 Key::Delete => {
639 self.move_cursor(&AKey::Right, false);
640 self.delete_char();
641 }
642 Key::Escape | Key::Char(REFRESH_SCREEN) => (),
643 Key::Char(EXIT) => {
644 if !self.dirty || self.quit_times + 1 >= self.config.quit_times {
645 return (true, None);
646 }
647 let r = self.config.quit_times - self.quit_times - 1;
648 set_status!(self, "Press Ctrl+Q {0} more time{1:.2$} to quit.", r, "s", r - 1);
649 reset_quit_times = false;
650 }
651 Key::Char(SAVE) => match self.file_name.take() {
652 Some(file_name) => {
654 self.save_and_handle_io_errors(&file_name);
655 self.file_name = Some(file_name);
656 }
657 None => prompt_mode = Some(PromptMode::Save(String::new())),
658 },
659 Key::Char(FIND) =>
660 prompt_mode = Some(PromptMode::Find(String::new(), self.cursor.clone(), None)),
661 Key::Char(GOTO) => prompt_mode = Some(PromptMode::GoTo(String::new())),
662 Key::Char(DUPLICATE) => self.duplicate_current_row(),
663 Key::Char(CUT) => {
664 self.copy_current_row();
665 self.delete_current_row();
666 }
667 Key::Char(COPY) => self.copy_current_row(),
668 Key::Char(PASTE) => self.paste_current_row(),
669 Key::Char(TOGGLE_COMMENT) => self.toggle_comment(),
670 Key::Char(EXECUTE) => prompt_mode = Some(PromptMode::Execute(String::new())),
671 Key::Char(c) => self.insert_byte(*c),
672 }
673 self.quit_times = if reset_quit_times { 0 } else { self.quit_times + 1 };
674 (false, prompt_mode)
675 }
676
677 fn find(&mut self, query: &str, last_match: Option<usize>, forward: bool) -> Option<usize> {
682 let num_rows = if query.is_empty() { 0 } else { self.rows.len() };
684 let mut current = last_match.unwrap_or_else(|| num_rows.saturating_sub(1));
685 for _ in 0..num_rows {
687 current = (current + if forward { 1 } else { num_rows - 1 }) % num_rows;
688 let row = &mut self.rows[current];
689 if let Some(cx) = row.chars.windows(query.len()).position(|w| w == query.as_bytes()) {
690 (self.cursor.x, self.cursor.y, self.cursor.coff) = (cx, current, 0);
694 let rx = row.cx2rx[cx];
695 row.match_segment = Some(rx..rx + query.len());
696 return Some(current);
697 }
698 }
699 None
700 }
701
702 pub fn run<I: BufRead>(&mut self, file_name: Option<&str>, input: &mut I) -> Result<(), Error> {
708 self.update_window_size()?;
709 set_status!(self, "{HELP_MESSAGE}");
710
711 if let Some(path) = file_name.map(sys::path) {
712 self.syntax = SyntaxConf::find(&path.to_string_lossy(), &sys::data_dirs());
713 self.load(path.as_path())?;
714 self.file_name = Some(path.to_string_lossy().to_string());
715 } else {
716 self.rows.push(Row::new(Vec::new()));
717 self.file_name = None;
718 }
719 loop {
720 if let Some(mode) = &self.prompt_mode {
721 set_status!(self, "{}", mode.status_msg());
722 }
723 self.refresh_screen()?;
724 let key = self.loop_until_keypress(input)?;
725 self.prompt_mode = match self.prompt_mode.take() {
727 None => match self.process_keypress(&key) {
729 (true, _) => return Ok(()),
730 (false, prompt_mode) => prompt_mode,
731 },
732 Some(prompt_mode) => prompt_mode.process_keypress(self, &key),
733 }
734 }
735 }
736}
737
738pub fn run<I: BufRead>(file_name: Option<&str>, input: &mut I) -> Result<(), Error> {
748 sys::register_winsize_change_signal_handler()?;
749 let orig_term_mode = sys::enable_raw_mode()?;
750 let mut editor = Editor { config: Config::load(), ..Default::default() };
751 editor.use_color = !std::env::var("NO_COLOR").is_ok_and(|val| !val.is_empty());
752
753 print!("{USE_ALTERNATE_SCREEN}");
754
755 let prev_hook = std::panic::take_hook();
756 std::panic::set_hook(Box::new(move |info| {
757 terminal::restore_terminal(&orig_term_mode).unwrap_or_else(|e| eprintln!("{e}"));
758 prev_hook(info);
759 }));
760
761 let result = editor.run(file_name, input);
762
763 terminal::restore_terminal(&orig_term_mode)?;
765
766 result
767}
768
769#[cfg_attr(test, derive(Debug, PartialEq))]
771enum PromptMode {
772 Save(String),
774 Find(String, CursorState, Option<usize>),
776 GoTo(String),
778 Execute(String),
780}
781
782impl PromptMode {
785 fn status_msg(&self) -> String {
787 match self {
788 Self::Save(buffer) => format!("Save as: {buffer}"),
789 Self::Find(buffer, ..) => format!("Search (Use ESC/Arrows/Enter): {buffer}"),
790 Self::GoTo(buffer) => format!("Enter line number[:column number]: {buffer}"),
791 Self::Execute(buffer) => format!("Command to execute: {buffer}"),
792 }
793 }
794
795 fn process_keypress(self, ed: &mut Editor, key: &Key) -> Option<Self> {
797 ed.status_msg = None;
798 match self {
799 Self::Save(b) => match process_prompt_keypress(b, key) {
800 PromptState::Active(b) => return Some(Self::Save(b)),
801 PromptState::Cancelled => set_status!(ed, "Save aborted"),
802 PromptState::Completed(file_name) => ed.save_as(file_name),
803 },
804 Self::Find(b, saved_cursor, last_match) => {
805 if let Some(row_idx) = last_match {
806 ed.rows[row_idx].match_segment = None;
807 }
808 match process_prompt_keypress(b, key) {
809 PromptState::Active(query) => {
810 #[expect(clippy::wildcard_enum_match_arm)]
811 let (last_match, forward) = match key {
812 Key::Arrow(AKey::Right | AKey::Down) | Key::Char(FIND) =>
813 (last_match, true),
814 Key::Arrow(AKey::Left | AKey::Up) => (last_match, false),
815 _ => (None, true),
816 };
817 let curr_match = ed.find(&query, last_match, forward);
818 return Some(Self::Find(query, saved_cursor, curr_match));
819 }
820 PromptState::Cancelled => ed.cursor = saved_cursor,
822 PromptState::Completed(_) => (),
824 }
825 }
826 Self::GoTo(b) => match process_prompt_keypress(b, key) {
827 PromptState::Active(b) => return Some(Self::GoTo(b)),
828 PromptState::Cancelled => (),
829 PromptState::Completed(b) => {
830 let mut split = b.splitn(2, ':')
831 .map(|u| u.trim().parse().map(|s: usize| s.saturating_sub(1)));
833 match (split.next().transpose(), split.next().transpose()) {
834 (Ok(Some(y)), Ok(x)) => {
835 ed.cursor.y = y.min(ed.rows.len());
836 if let Some(rx) = x {
837 ed.cursor.x = ed.current_row().map_or(0, |r| r.rx2cx[rx]);
838 } else {
839 ed.update_cursor_x_position();
840 }
841 }
842 (Err(e), _) | (_, Err(e)) => set_status!(ed, "Parsing error: {e}"),
843 (Ok(None), _) => (),
844 }
845 }
846 },
847 Self::Execute(b) => match process_prompt_keypress(b, key) {
848 PromptState::Active(b) => return Some(Self::Execute(b)),
849 PromptState::Cancelled => (),
850 PromptState::Completed(b) => {
851 let mut args = b.split_whitespace();
852 match Command::new(args.next().unwrap_or_default()).args(args).output() {
853 Ok(out) if !out.status.success() =>
854 set_status!(ed, "{}", String::from_utf8_lossy(&out.stderr).trim_end()),
855 Ok(out) => out.stdout.into_iter().for_each(|c| match c {
856 b'\n' => ed.insert_new_line(),
857 c => ed.insert_byte(c),
858 }),
859 Err(e) => set_status!(ed, "{e}"),
860 }
861 }
862 },
863 }
864 None
865 }
866}
867
868#[cfg_attr(test, derive(Debug, PartialEq))]
870enum PromptState {
871 Active(String),
873 Completed(String),
875 Cancelled,
876}
877
878fn process_prompt_keypress(mut buffer: String, key: &Key) -> PromptState {
880 #[expect(clippy::wildcard_enum_match_arm)]
881 match key {
882 Key::Char(b'\r') => return PromptState::Completed(buffer),
883 Key::Escape | Key::Char(EXIT) => return PromptState::Cancelled,
884 Key::Char(BACKSPACE | DELETE_BIS) => _ = buffer.pop(),
885 Key::Char(c @ 0..=126) if !c.is_ascii_control() => buffer.push(*c as char),
886 _ => (),
888 }
889 PromptState::Active(buffer)
890}
891
892#[cfg(test)]
893mod tests {
894 use std::io::Cursor;
895
896 use rstest::rstest;
897
898 use super::*;
899 use crate::syntax::HlType;
900
901 fn assert_row_chars_equal(editor: &Editor, expected: &[&[u8]]) {
902 assert_eq!(
903 editor.rows.len(),
904 expected.len(),
905 "editor has {} rows, expected {}",
906 editor.rows.len(),
907 expected.len()
908 );
909 for (i, (row, expected)) in editor.rows.iter().zip(expected).enumerate() {
910 assert_eq!(
911 row.chars,
912 *expected,
913 "comparing characters for row {}\n left: {}\n right: {}",
914 i,
915 String::from_utf8_lossy(&row.chars),
916 String::from_utf8_lossy(expected)
917 );
918 }
919 }
920
921 fn assert_row_synthax_highlighting_types_equal(editor: &Editor, expected: &[&[HlType]]) {
922 assert_eq!(
923 editor.rows.len(),
924 expected.len(),
925 "editor has {} rows, expected {}",
926 editor.rows.len(),
927 expected.len()
928 );
929 for (i, (row, expected)) in editor.rows.iter().zip(expected).enumerate() {
930 assert_eq!(row.hl, *expected, "comparing HlTypes for row {i}");
931 }
932 }
933
934 #[rstest]
935 #[case(0, "0B")]
936 #[case(1, "1B")]
937 #[case(1023, "1023B")]
938 #[case(1024, "1.00kB")]
939 #[case(1536, "1.50kB")]
940 #[case(21 * 1024 - 11, "20.98kB")]
942 #[case(21 * 1024 - 10, "20.99kB")]
943 #[case(21 * 1024 - 3, "20.99kB")]
944 #[case(21 * 1024, "21.00kB")]
945 #[case(21 * 1024 + 3, "21.00kB")]
946 #[case(21 * 1024 + 10, "21.00kB")]
947 #[case(21 * 1024 + 11, "21.01kB")]
948 #[case(1024 * 1024 - 1, "1023.99kB")]
949 #[case(1024 * 1024, "1.00MB")]
950 #[case(1024 * 1024 + 1, "1.00MB")]
951 #[case(100 * 1024 * 1024 * 1024, "100.00GB")]
952 #[case(313 * 1024 * 1024 * 1024 * 1024, "313.00TB")]
953 fn format_size_output(#[case] input: u64, #[case] expected_output: &str) {
954 assert_eq!(format_size(input), expected_output);
955 }
956
957 #[test]
958 fn editor_insert_byte() {
959 let mut editor = Editor::default();
960 let editor_cursor_x_before = editor.cursor.x;
961
962 editor.insert_byte(b'X');
963 editor.insert_byte(b'Y');
964 editor.insert_byte(b'Z');
965
966 assert_eq!(editor.cursor.x, editor_cursor_x_before + 3);
967 assert_eq!(editor.rows.len(), 1);
968 assert_eq!(editor.n_bytes, 3);
969 assert_eq!(editor.rows[0].chars, [b'X', b'Y', b'Z']);
970 }
971
972 #[test]
973 fn editor_insert_new_line() {
974 let mut editor = Editor::default();
975 let editor_cursor_y_before = editor.cursor.y;
976
977 for _ in 0..3 {
978 editor.insert_new_line();
979 }
980
981 assert_eq!(editor.cursor.y, editor_cursor_y_before + 3);
982 assert_eq!(editor.rows.len(), 3);
983 assert_eq!(editor.n_bytes, 0);
984
985 for row in &editor.rows {
986 assert_eq!(row.chars, []);
987 }
988 }
989
990 #[test]
991 fn editor_delete_char() {
992 let mut editor = Editor::default();
993 for b in b"Hello world!" {
994 editor.insert_byte(*b);
995 }
996 editor.delete_char();
997 assert_row_chars_equal(&editor, &[b"Hello world"]);
998 editor.move_cursor(&AKey::Left, true);
999 editor.move_cursor(&AKey::Left, false);
1000 editor.move_cursor(&AKey::Left, false);
1001 editor.delete_char();
1002 assert_row_chars_equal(&editor, &[b"Helo world"]);
1003 }
1004
1005 #[test]
1006 fn editor_delete_next_char() {
1007 let mut editor = Editor::default();
1008 for &b in b"Hello world!\nHappy New Year!" {
1009 editor.process_keypress(&Key::Char(b));
1010 }
1011 editor.process_keypress(&Key::Delete);
1012 assert_row_chars_equal(&editor, &[b"Hello world!", b"Happy New Year!"]);
1013 editor.move_cursor(&AKey::Left, true);
1014 editor.process_keypress(&Key::Delete);
1015 assert_row_chars_equal(&editor, &[b"Hello world!", b"Happy New ear!"]);
1016 editor.move_cursor(&AKey::Left, true);
1017 editor.move_cursor(&AKey::Left, true);
1018 editor.move_cursor(&AKey::Left, true);
1019 editor.process_keypress(&Key::Delete);
1020 assert_row_chars_equal(&editor, &[b"Hello world!Happy New ear!"]);
1021 }
1022
1023 #[test]
1024 fn editor_move_cursor_left() {
1025 let mut editor = Editor::default();
1026 for &b in b"Hello world!\nHappy New Year!" {
1027 editor.process_keypress(&Key::Char(b));
1028 }
1029
1030 assert_eq!(editor.cursor.x, 15);
1032 assert_eq!(editor.cursor.y, 1);
1033
1034 editor.move_cursor(&AKey::Left, true);
1035 assert_eq!(editor.cursor.x, 10);
1036 assert_eq!(editor.cursor.y, 1);
1037
1038 editor.move_cursor(&AKey::Left, false);
1039 assert_eq!(editor.cursor.x, 9);
1040 assert_eq!(editor.cursor.y, 1);
1041
1042 editor.move_cursor(&AKey::Left, true);
1043 assert_eq!(editor.cursor.x, 6);
1044 assert_eq!(editor.cursor.y, 1);
1045
1046 editor.move_cursor(&AKey::Left, true);
1047 assert_eq!(editor.cursor.x, 0);
1048 assert_eq!(editor.cursor.y, 1);
1049
1050 editor.move_cursor(&AKey::Left, false);
1051 assert_eq!(editor.cursor.x, 12);
1052 assert_eq!(editor.cursor.y, 0);
1053
1054 editor.move_cursor(&AKey::Left, true);
1055 assert_eq!(editor.cursor.x, 6);
1056 assert_eq!(editor.cursor.y, 0);
1057
1058 editor.move_cursor(&AKey::Left, true);
1059 assert_eq!(editor.cursor.x, 0);
1060 assert_eq!(editor.cursor.y, 0);
1061
1062 editor.move_cursor(&AKey::Left, false);
1063 assert_eq!(editor.cursor.x, 0);
1064 assert_eq!(editor.cursor.y, 0);
1065 }
1066
1067 #[test]
1068 fn editor_move_cursor_up() {
1069 let mut editor = Editor::default();
1070 for &b in b"abcdefgh\nij\nklmnopqrstuvwxyz" {
1071 editor.process_keypress(&Key::Char(b));
1072 }
1073
1074 assert_eq!(editor.cursor.x, 16);
1076 assert_eq!(editor.cursor.y, 2);
1077
1078 editor.move_cursor(&AKey::Up, false);
1079 assert_eq!(editor.cursor.x, 2);
1080 assert_eq!(editor.cursor.y, 1);
1081
1082 editor.move_cursor(&AKey::Up, true);
1083 assert_eq!(editor.cursor.x, 2);
1084 assert_eq!(editor.cursor.y, 0);
1085
1086 editor.move_cursor(&AKey::Up, false);
1087 assert_eq!(editor.cursor.x, 2);
1088 assert_eq!(editor.cursor.y, 0);
1089 }
1090
1091 #[test]
1092 fn editor_move_cursor_right() {
1093 let mut editor = Editor::default();
1094 for &b in b"Hello world\nHappy New Year" {
1095 editor.process_keypress(&Key::Char(b));
1096 }
1097
1098 assert_eq!(editor.cursor.x, 14);
1100 assert_eq!(editor.cursor.y, 1);
1101
1102 editor.move_cursor(&AKey::Right, false);
1103 assert_eq!(editor.cursor.x, 0);
1104 assert_eq!(editor.cursor.y, 2);
1105
1106 editor.move_cursor(&AKey::Right, false);
1107 assert_eq!(editor.cursor.x, 0);
1108 assert_eq!(editor.cursor.y, 2);
1109
1110 editor.move_cursor(&AKey::Up, true);
1111 editor.move_cursor(&AKey::Up, true);
1112 assert_eq!(editor.cursor.x, 0);
1113 assert_eq!(editor.cursor.y, 0);
1114
1115 editor.move_cursor(&AKey::Right, true);
1116 assert_eq!(editor.cursor.x, 5);
1117 assert_eq!(editor.cursor.y, 0);
1118
1119 editor.move_cursor(&AKey::Right, true);
1120 assert_eq!(editor.cursor.x, 11);
1121 assert_eq!(editor.cursor.y, 0);
1122
1123 editor.move_cursor(&AKey::Right, false);
1124 assert_eq!(editor.cursor.x, 0);
1125 assert_eq!(editor.cursor.y, 1);
1126 }
1127
1128 #[test]
1129 fn editor_move_cursor_down() {
1130 let mut editor = Editor::default();
1131 for &b in b"abcdefgh\nij\nklmnopqrstuvwxyz" {
1132 editor.process_keypress(&Key::Char(b));
1133 }
1134
1135 assert_eq!(editor.cursor.x, 16);
1137 assert_eq!(editor.cursor.y, 2);
1138
1139 editor.move_cursor(&AKey::Down, false);
1140 assert_eq!(editor.cursor.x, 0);
1141 assert_eq!(editor.cursor.y, 3);
1142
1143 editor.move_cursor(&AKey::Up, false);
1144 editor.move_cursor(&AKey::Up, false);
1145 editor.move_cursor(&AKey::Up, false);
1146
1147 assert_eq!(editor.cursor.x, 0);
1148 assert_eq!(editor.cursor.y, 0);
1149
1150 editor.move_cursor(&AKey::Right, true);
1151 assert_eq!(editor.cursor.x, 8);
1152 assert_eq!(editor.cursor.y, 0);
1153
1154 editor.move_cursor(&AKey::Down, true);
1155 assert_eq!(editor.cursor.x, 2);
1156 assert_eq!(editor.cursor.y, 1);
1157
1158 editor.move_cursor(&AKey::Down, true);
1159 assert_eq!(editor.cursor.x, 2);
1160 assert_eq!(editor.cursor.y, 2);
1161
1162 editor.move_cursor(&AKey::Down, true);
1163 assert_eq!(editor.cursor.x, 0);
1164 assert_eq!(editor.cursor.y, 3);
1165
1166 editor.move_cursor(&AKey::Down, false);
1167 assert_eq!(editor.cursor.x, 0);
1168 assert_eq!(editor.cursor.y, 3);
1169 }
1170
1171 #[test]
1172 fn editor_press_home_key() {
1173 let mut editor = Editor::default();
1174 for &b in b"Hello\nWorld\nand\nFerris!" {
1175 editor.process_keypress(&Key::Char(b));
1176 }
1177
1178 assert_eq!(editor.cursor.x, 7);
1180 assert_eq!(editor.cursor.y, 3);
1181
1182 editor.process_keypress(&Key::Home);
1183 assert_eq!(editor.cursor.x, 0);
1184 assert_eq!(editor.cursor.y, 3);
1185
1186 editor.move_cursor(&AKey::Up, false);
1187 editor.move_cursor(&AKey::Up, false);
1188 editor.move_cursor(&AKey::Up, false);
1189
1190 assert_eq!(editor.cursor.x, 0);
1191 assert_eq!(editor.cursor.y, 0);
1192
1193 editor.move_cursor(&AKey::Right, true);
1194 assert_eq!(editor.cursor.x, 5);
1195 assert_eq!(editor.cursor.y, 0);
1196
1197 editor.process_keypress(&Key::Home);
1198 assert_eq!(editor.cursor.x, 0);
1199 assert_eq!(editor.cursor.y, 0);
1200 }
1201
1202 #[test]
1203 fn editor_press_end_key() {
1204 let mut editor = Editor::default();
1205 for &b in b"Hello\nWorld\nand\nFerris!" {
1206 editor.process_keypress(&Key::Char(b));
1207 }
1208
1209 assert_eq!(editor.cursor.x, 7);
1211 assert_eq!(editor.cursor.y, 3);
1212
1213 editor.process_keypress(&Key::End);
1214 assert_eq!(editor.cursor.x, 7);
1215 assert_eq!(editor.cursor.y, 3);
1216
1217 editor.move_cursor(&AKey::Up, false);
1218 editor.move_cursor(&AKey::Up, false);
1219 editor.move_cursor(&AKey::Up, false);
1220
1221 assert_eq!(editor.cursor.x, 3);
1222 assert_eq!(editor.cursor.y, 0);
1223
1224 editor.process_keypress(&Key::End);
1225 assert_eq!(editor.cursor.x, 5);
1226 assert_eq!(editor.cursor.y, 0);
1227 }
1228
1229 #[test]
1230 fn editor_page_up_moves_cursor_to_viewport_top() {
1231 let mut editor = Editor { screen_rows: 4, ..Default::default() };
1232 for _ in 0..10 {
1233 editor.insert_new_line();
1234 }
1235
1236 (editor.cursor.y, editor.cursor.x) = (3, 0);
1237 editor.insert_byte(b'a');
1238 editor.insert_byte(b'b');
1239
1240 (editor.cursor.y, editor.cursor.x, editor.cursor.roff) = (9, 5, 7);
1241 let (should_quit, prompt_mode) = editor.process_keypress(&Key::PageUp);
1242
1243 assert!(!should_quit);
1244 assert!(prompt_mode.is_none());
1245 assert_eq!(editor.cursor.y, 3);
1246 assert_eq!(editor.cursor.x, 2);
1247 }
1248
1249 #[test]
1250 fn editor_page_down_moves_cursor_to_viewport_bottom() {
1251 let mut editor = Editor { screen_rows: 4, ..Default::default() };
1252 for _ in 0..12 {
1253 editor.insert_new_line();
1254 }
1255
1256 (editor.cursor.y, editor.cursor.x) = (11, 0);
1257 editor.insert_byte(b'x');
1258 editor.insert_byte(b'y');
1259 editor.insert_byte(b'z');
1260
1261 (editor.cursor.x, editor.cursor.roff) = (6, 4);
1262 let (should_quit, prompt_mode) = editor.process_keypress(&Key::PageDown);
1263 assert!(!should_quit);
1264 assert!(prompt_mode.is_none());
1265 assert_eq!(editor.cursor.y, 11);
1266 assert_eq!(editor.cursor.x, 3);
1267
1268 (editor.cursor.x, editor.cursor.roff) = (9, 11);
1269 let (should_quit, prompt_mode_again) = editor.process_keypress(&Key::PageDown);
1270 assert!(!should_quit);
1271 assert!(prompt_mode_again.is_none());
1272 assert_eq!(editor.cursor.y, editor.rows.len());
1273 assert_eq!(editor.cursor.x, 0);
1274 }
1275
1276 #[rstest]
1277 #[case::beginning_of_first_row(b"Hello\nWorld!\n", (0, 0), &[&b"World!"[..], &b""[..]], 0)]
1278 #[case::middle_of_first_row(b"Hello\nWorld!\n", (3, 0), &[&b"World!"[..], &b""[..]], 0)]
1279 #[case::end_of_first_row(b"Hello\nWorld!\n", (5, 0), &[&b"World!"[..], &b""[..]], 0)]
1280 #[case::empty_first_row(b"\nHello", (0, 0), &[&b"Hello"[..]], 0)]
1281 #[case::beginning_of_only_row(b"Hello", (0, 0), &[&b""[..]], 0)]
1282 #[case::middle_of_only_row(b"Hello", (3, 0), &[&b""[..]], 0)]
1283 #[case::end_of_only_row(b"Hello", (5, 0), &[&b""[..]], 0)]
1284 #[case::beginning_of_middle_row(b"Hello\nWorld!\n", (0, 1), &[&b"Hello"[..], &b""[..]], 1)]
1285 #[case::middle_of_middle_row(b"Hello\nWorld!\n", (3, 1), &[&b"Hello"[..], &b""[..]], 1)]
1286 #[case::end_of_middle_row(b"Hello\nWorld!\n", (6, 1), &[&b"Hello"[..], &b""[..]], 1)]
1287 #[case::empty_middle_row(b"Hello\n\nWorld!", (0, 1), &[&b"Hello"[..], &b"World!"[..]], 1)]
1288 #[case::beginning_of_last_row(b"Hello\nWorld!", (0, 1), &[&b"Hello"[..]], 0)]
1289 #[case::middle_of_last_row(b"Hello\nWorld!", (3, 1), &[&b"Hello"[..]], 0)]
1290 #[case::end_of_last_row(b"Hello\nWorld!", (6, 1), &[&b"Hello"[..]], 0)]
1291 #[case::empty_last_row(b"Hello\n", (0, 1), &[&b"Hello"[..]], 0)]
1292 #[case::after_last_row(b"Hello\nWorld!", (0, 2), &[&b"Hello"[..], &b"World!"[..]], 2)]
1293 fn delete_current_row_updates_buffer_and_position(
1294 #[case] initial_buffer: &[u8], #[case] cursor_position: (usize, usize),
1295 #[case] expected_rows: &[&[u8]], #[case] expected_cursor_row: usize,
1296 ) {
1297 let mut editor = Editor::default();
1298 for &b in initial_buffer {
1299 editor.process_keypress(&Key::Char(b));
1300 }
1301 (editor.cursor.x, editor.cursor.y) = cursor_position;
1302
1303 editor.delete_current_row();
1304
1305 assert_row_chars_equal(&editor, expected_rows);
1306 assert_eq!(
1307 (editor.cursor.x, editor.cursor.y),
1308 (0, expected_cursor_row),
1309 "cursor is at {}:{}, expected {}:0",
1310 editor.cursor.y,
1311 editor.cursor.x,
1312 expected_cursor_row
1313 );
1314 }
1315
1316 #[rstest]
1317 #[case::first_row(0)]
1318 #[case::middle_row(5)]
1319 #[case::last_row(9)]
1320 fn delete_current_row_updates_screen_cols_and_ln_pad(#[case] current_row: usize) {
1321 let mut editor = Editor { window_width: 100, ..Default::default() };
1322 for _ in 0..10 {
1323 editor.insert_new_line();
1324 }
1325 assert_eq!(editor.screen_cols, 96);
1326 assert_eq!(editor.ln_pad, 4);
1327
1328 editor.cursor.y = current_row;
1329 editor.delete_current_row();
1330
1331 assert_eq!(editor.screen_cols, 97);
1332 assert_eq!(editor.ln_pad, 3);
1333 }
1334
1335 #[test]
1336 fn delete_current_row_updates_syntax_highlighting() {
1337 let mut editor = Editor {
1338 syntax: SyntaxConf {
1339 ml_comment_delims: Some(("/*".to_owned(), "*/".to_owned())),
1340 ..Default::default()
1341 },
1342 ..Default::default()
1343 };
1344 for &b in b"A\nb/*c\nd\ne\nf*/g\nh" {
1345 editor.process_keypress(&Key::Char(b));
1346 }
1347
1348 assert_row_chars_equal(&editor, &[b"A", b"b/*c", b"d", b"e", b"f*/g", b"h"]);
1349 assert_row_synthax_highlighting_types_equal(&editor, &[
1350 &[HlType::Normal],
1351 &[HlType::Normal, HlType::MlComment, HlType::MlComment, HlType::MlComment],
1352 &[HlType::MlComment],
1353 &[HlType::MlComment],
1354 &[HlType::MlComment, HlType::MlComment, HlType::MlComment, HlType::Normal],
1355 &[HlType::Normal],
1356 ]);
1357
1358 (editor.cursor.x, editor.cursor.y) = (0, 4);
1359 editor.delete_current_row();
1360
1361 assert_row_chars_equal(&editor, &[b"A", b"b/*c", b"d", b"e", b"h"]);
1362 assert_row_synthax_highlighting_types_equal(&editor, &[
1363 &[HlType::Normal],
1364 &[HlType::Normal, HlType::MlComment, HlType::MlComment, HlType::MlComment],
1365 &[HlType::MlComment],
1366 &[HlType::MlComment],
1367 &[HlType::MlComment],
1368 ]);
1369
1370 (editor.cursor.x, editor.cursor.y) = (0, 1);
1371 editor.delete_current_row();
1372
1373 assert_row_chars_equal(&editor, &[b"A", b"d", b"e", b"h"]);
1374 assert_row_synthax_highlighting_types_equal(&editor, &[
1375 &[HlType::Normal],
1376 &[HlType::Normal],
1377 &[HlType::Normal],
1378 &[HlType::Normal],
1379 ]);
1380 }
1381
1382 #[test]
1383 fn loop_until_keypress() -> Result<(), Error> {
1384 let mut editor = Editor::default();
1385 let mut fake_stdin = Cursor::new(
1386 b"abc\x1b[A\x1b[B\x1b[C\x1b[D\x1b[H\x1bOH\x1b[F\x1bOF\x1b[1;5C\x1b[5C\x1b[99",
1387 );
1388 for expected_key in [
1389 Key::Char(b'a'),
1390 Key::Char(b'b'),
1391 Key::Char(b'c'),
1392 Key::Arrow(AKey::Up),
1393 Key::Arrow(AKey::Down),
1394 Key::Arrow(AKey::Right),
1395 Key::Arrow(AKey::Left),
1396 Key::Home,
1397 Key::Home,
1398 Key::End,
1399 Key::End,
1400 Key::CtrlArrow(AKey::Right),
1401 Key::CtrlArrow(AKey::Right),
1402 Key::Escape,
1403 ] {
1404 assert_eq!(editor.loop_until_keypress(&mut fake_stdin)?, expected_key);
1405 }
1406 Ok(())
1407 }
1408
1409 #[rstest]
1410 #[case::ascii_completed(&[Key::Char(b'H'), Key::Char(b'i'), Key::Char(b'\r')], &PromptState::Completed(String::from("Hi")))]
1411 #[case::escape(&[Key::Char(b'H'), Key::Char(b'i'), Key::Escape], &PromptState::Cancelled)]
1412 #[case::exit(&[Key::Char(b'H'), Key::Char(b'i'), Key::Char(EXIT)], &PromptState::Cancelled)]
1413 #[case::skip_ascii_control(&[Key::Char(b'\x0A')], &PromptState::Active(String::new()))]
1414 #[case::unsupported_non_ascii(&[Key::Char(b'\xEF')], &PromptState::Active(String::new()))]
1415 #[case::backspace(&[Key::Char(b'H'), Key::Char(b'i'), Key::Char(BACKSPACE), Key::Char(BACKSPACE)], &PromptState::Active(String::new()))]
1416 #[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()))]
1417 fn process_prompt_keypresses(#[case] keys: &[Key], #[case] expected_final_state: &PromptState) {
1418 let mut prompt_state = PromptState::Active(String::new());
1419 for key in keys {
1420 if let PromptState::Active(buffer) = prompt_state {
1421 prompt_state = process_prompt_keypress(buffer, key);
1422 } else {
1423 panic!("Prompt state: {prompt_state:?} is not active")
1424 }
1425 }
1426 assert_eq!(prompt_state, *expected_final_state);
1427 }
1428
1429 #[rstest]
1430 #[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")]
1431 #[case(&[Key::Char(b'H'), Key::Char(b'i'), Key::Char(BACKSPACE), Key::Char(BACKSPACE), Key::Char(BACKSPACE)], "")]
1432 fn process_find_keypress_completed(#[case] keys: &[Key], #[case] expected_final_value: &str) {
1433 let mut ed: Editor = Editor::default();
1434 ed.insert_new_line();
1435 let mut prompt_mode = Some(PromptMode::Find(String::new(), CursorState::default(), None));
1436 for key in keys {
1437 prompt_mode = prompt_mode
1438 .take()
1439 .and_then(|prompt_mode| prompt_mode.process_keypress(&mut ed, key));
1440 }
1441 assert_eq!(
1442 prompt_mode,
1443 Some(PromptMode::Find(
1444 String::from(expected_final_value),
1445 CursorState::default(),
1446 None
1447 ))
1448 );
1449 prompt_mode = prompt_mode
1450 .take()
1451 .and_then(|prompt_mode| prompt_mode.process_keypress(&mut ed, &Key::Char(b'\r')));
1452 assert_eq!(prompt_mode, None);
1453 }
1454
1455 #[rstest]
1456 #[case(100, true, 12345, "\u{1b}[38;5;240m12345 │\u{1b}[m")]
1457 #[case(100, true, "~", "\u{1b}[38;5;240m~ │\u{1b}[m")]
1458 #[case(10, true, 12345, "")]
1459 #[case(10, true, "~", "")]
1460 #[case(100, false, 12345, "12345 │")]
1461 #[case(100, false, "~", "~ │")]
1462 #[case(10, false, 12345, "")]
1463 #[case(10, false, "~", "")]
1464 fn draw_left_padding<T: Display>(
1465 #[case] window_width: usize, #[case] use_color: bool, #[case] value: T,
1466 #[case] expected: &'static str,
1467 ) {
1468 let mut editor = Editor { window_width, use_color, ..Default::default() };
1469 editor.update_screen_cols();
1470
1471 let mut buffer = String::new();
1472 editor.draw_left_padding(&mut buffer, value);
1473 assert_eq!(buffer, expected);
1474 }
1475
1476 #[test]
1477 fn editor_toggle_comment() {
1478 let mut editor = Editor::default();
1479
1480 editor.syntax.sl_comment_start = vec!["#".to_owned()];
1482
1483 for b in b"def hello():\n print(\"Hello\")\n return True" {
1484 if *b == b'\n' {
1485 editor.insert_new_line();
1486 } else {
1487 editor.insert_byte(*b);
1488 }
1489 }
1490
1491 editor.cursor.y = 0; editor.cursor.x = 0;
1494 editor.process_keypress(&Key::Char(TOGGLE_COMMENT));
1495 assert_eq!(editor.rows[0].chars, b"# def hello():");
1496
1497 editor.process_keypress(&Key::Char(TOGGLE_COMMENT));
1499 assert_eq!(editor.rows[0].chars, b"def hello():");
1500
1501 editor.cursor.y = 1; editor.cursor.x = 0;
1504 editor.process_keypress(&Key::Char(TOGGLE_COMMENT));
1505 assert_eq!(editor.rows[1].chars, b" # print(\"Hello\")");
1506
1507 editor.process_keypress(&Key::Char(TOGGLE_COMMENT));
1509 assert_eq!(editor.rows[1].chars, b" print(\"Hello\")");
1510
1511 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():");
1516
1517 editor.cursor.x = editor.rows[0].chars.len(); editor.process_keypress(&Key::Char(TOGGLE_COMMENT)); assert_eq!(editor.rows[0].chars, b"def hello():");
1521
1522 assert!(editor.cursor.x <= editor.rows[0].chars.len());
1524 }
1525}