kibi/
editor.rs

1// SPDX-FileCopyrightText: 2020 Ilaï Deutel & Kibi Contributors
2//
3// SPDX-License-Identifier: MIT OR Apache-2.0
4
5use 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
33/// `set_status!` sets a formatted status message for the editor.
34/// Example usage: `set_status!(editor, "{file_size} written to {file_name}")`
35macro_rules! set_status { ($editor:expr, $($arg:expr),*) => ($editor.status_msg = Some(StatusMessage::new(format!($($arg),*)))) }
36
37/// Enum of input keys
38#[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/// Enum of arrow keys
52#[cfg_attr(test, derive(Debug, PartialEq))]
53enum AKey {
54    Left,
55    Right,
56    Up,
57    Down,
58}
59
60/// Describes the cursor position and the screen offset
61#[derive(Debug, Default, Clone, PartialEq)]
62struct CursorState {
63    /// x position (indexing the characters, not the columns)
64    x: usize,
65    /// y position (row number, 0-indexed)
66    y: usize,
67    /// Row offset
68    roff: usize,
69    /// Column offset
70    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    /// Scroll the terminal window vertically and horizontally (i.e. adjusting
77    /// the row offset and the column offset) so that the cursor can be
78    /// shown.
79    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/// The `Editor` struct, contains the state and configuration of the text
86/// editor.
87#[derive(Default)]
88pub struct Editor {
89    /// If not `None`, the current prompt mode (`Save`, `Find`, `GoTo`, or
90    /// `Execute`). If `None`, we are in regular edition mode.
91    prompt_mode: Option<PromptMode>,
92    /// The current state of the cursor.
93    cursor: CursorState,
94    /// The padding size used on the left for line numbering.
95    ln_pad: usize,
96    /// The width of the current window. Will be updated when the window is
97    /// resized.
98    window_width: usize,
99    /// The number of rows that can be used for the editor, excluding the status
100    /// bar and the message bar
101    screen_rows: usize,
102    /// The number of columns that can be used for the editor, excluding the
103    /// part used for line numbers
104    screen_cols: usize,
105    /// The collection of rows, including the content and the syntax
106    /// highlighting information.
107    rows: Vec<Row>,
108    /// Whether the document has been modified since it was open.
109    dirty: bool,
110    /// The configuration for the editor.
111    config: Config,
112    /// The number of consecutive times the user has tried to quit without
113    /// saving. After `config.quit_times`, the program will exit.
114    quit_times: usize,
115    /// The file name. If None, the user will be prompted for a file name the
116    /// first time they try to save.
117    // TODO: It may be better to store a PathBuf instead
118    file_name: Option<String>,
119    /// The current status message being shown.
120    status_msg: Option<StatusMessage>,
121    /// The syntax configuration corresponding to the current file's extension.
122    syntax: SyntaxConf,
123    /// The number of bytes contained in `rows`. This excludes new lines.
124    n_bytes: u64,
125    /// The copied buffer of a row
126    copied_row: Vec<u8>,
127    /// Whether to use ANSI color escape codes for rendering
128    use_color: bool,
129}
130
131/// Describes a status message, shown at the bottom at the screen.
132struct StatusMessage {
133    /// The message to display.
134    msg: String,
135    /// The `Instant` the status message was first displayed.
136    time: Instant,
137}
138
139impl StatusMessage {
140    /// Create a new status message and set time to the current date/time.
141    fn new(msg: String) -> Self { Self { msg, time: Instant::now() } }
142}
143
144/// Pretty-format a size in bytes.
145fn format_size(n: u64) -> String {
146    if n < 1024 {
147        return format!("{n}B");
148    }
149    // i is the largest value such that 1024 ^ i < n
150    let i = n.ilog2() / 10;
151
152    // Compute the size with two decimal places (rounded down) as the last two
153    // digits of q This avoid float formatting reducing the binary size
154    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
158/// Return an Arrow Key given an ANSI code.
159///
160/// The argument must be a valide arrow key ANSI code (`a`, `b`, `c` or `d`),
161/// case-insensitive).
162fn 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    /// Return the current row if the cursor points to an existing row, `None`
174    /// otherwise.
175    fn current_row(&self) -> Option<&Row> { self.rows.get(self.cursor.y) }
176
177    /// Return the position of the cursor, in terms of rendered characters (as
178    /// opposed to `self.cursor.x`, which is the position of the cursor in
179    /// terms of bytes).
180    fn rx(&self) -> usize { self.current_row().map_or(0, |r| r.cx2rx[self.cursor.x]) }
181
182    /// Move the cursor following an arrow key (← → ↑ ↓).
183    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                // ← moving to previous word
188                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            // ← at the beginning of the line: move to the end of the previous line. The x
194            // position will be adjusted after this `match` to accommodate the current row
195            // length, so we can just set here to the maximum possible value here.
196            (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                // → moving to next word
201                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            // TODO: For Up and Down, move self.cursor.x to be consistent with tabs and UTF-8
208            //  characters, i.e. according to rx
209            (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    /// Update the cursor x position. If the cursor y position has changed, the
217    /// current position might be illegal (x is further right than the last
218    /// character of the row). If that is the case, clamp `self.cursor.x`.
219    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    /// Run a loop to obtain the key that was pressed. At each iteration of the
224    /// loop (until a key is pressed), we listen to the `ws_changed` channel
225    /// to check if a window size change signal has been received. When
226    /// bytes are received, we match to a corresponding `Key`. In particular,
227    /// we handle ANSI escape codes to return `Key::Delete`, `Key::Home` etc.
228    fn loop_until_keypress(&mut self, input: &mut impl BufRead) -> Result<Key, Error> {
229        let mut bytes = input.bytes();
230        loop {
231            // Handle window size if a signal has be received
232            if sys::has_window_size_changed() {
233                self.update_window_size()?;
234                self.refresh_screen()?;
235            }
236            // Match on the next byte received or, if the first byte is <ESC> ('\x1b'), on
237            // the next few bytes.
238            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                                // 1 is the default modifier value. Therefore, <ESC>[1;5C is
251                                // equivalent to <ESC>[5C, etc.
252                                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    /// Update the `screen_rows`, `window_width`, `screen_cols` and `ln_padding`
275    /// attributes.
276    fn update_window_size(&mut self) -> Result<(), Error> {
277        let wsize = sys::get_window_size().or_else(|_| terminal::get_window_size_using_cursor())?;
278        // Make room for the status bar and status message
279        (self.screen_rows, self.window_width) = (wsize.0.saturating_sub(2), wsize.1);
280        self.update_screen_cols();
281        Ok(())
282    }
283
284    /// Update the `screen_cols` and `ln_padding` attributes based on the
285    /// maximum number of digits for line numbers (since the left padding
286    /// depends on this number of digits).
287    fn update_screen_cols(&mut self) {
288        // The maximum number of digits to use for the line number is the number of
289        // digits of the last line number. This is equal to the number of times
290        // we can divide this number by ten, computed below using `successors`.
291        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    /// Update a row, given its index. If `ignore_following_rows` is `false` and
298    /// the highlight state has changed during the update (for instance, it
299    /// is now in "multi-line comment" state, keep updating the next rows
300    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            // If the state has changed (for instance, a multi-line comment
309            // started in this row), continue updating the following
310            // rows
311        }
312    }
313
314    /// Update all the rows.
315    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    /// Insert a byte at the current cursor position. If there is no row at the
323    /// current cursor position, add a new row and insert the byte.
324    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            // The number of rows has changed. The left padding may need to be updated.
330            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    /// Insert a new line at the current cursor position and move the cursor to
337    /// the start of the new line. If the cursor is in the middle of a row,
338    /// split off that row.
339    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            // self.rows[self.cursor.y] must exist, since cursor.x = 0 for any cursor.y ≥
344            // row.len()
345            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    /// Delete a character at the current cursor position. If the cursor is
357    /// located at the beginning of a row that is not the first or last row,
358    /// merge the current row and the previous row. If the cursor is located
359    /// after the last row, move up to the last character of the previous row.
360    fn delete_char(&mut self) {
361        if self.cursor.x > 0 {
362            let row = &mut self.rows[self.cursor.y];
363            // Obtain the number of bytes to be removed: could be 1-4 (UTF-8 character
364            // size).
365            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            // The number of rows has changed. The left padding may need to be updated.
379            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            // If the cursor is located after the last row, pressing backspace is equivalent
383            // to pressing the left arrow key.
384            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    /// Toggle comment on the current line using the appropriate comment symbol
422    /// from the syntax configuration. If the line is already commented,
423    /// uncomment it. If not, add a comment symbol at the beginning.
424    fn toggle_comment(&mut self) {
425        // Get the first single-line comment start symbol from syntax config
426        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        // Find the first non-whitespace character position
429        let pos = row.chars.iter().position(|&c| !(c as char).is_whitespace()).unwrap_or(0);
430
431        // Check if the line is already commented
432        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            // Remove the comment and return the removed size as a negative integer
435            0isize.saturating_sub_unsigned(row.chars.drain(pos..pos + to_remove).len())
436        } else {
437            // Insert comment at the first non-whitespace position
438            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        // Update cursor position to ensure it's valid after row update
448        self.update_cursor_x_position();
449        self.dirty = true;
450    }
451
452    /// Try to load a file. If found, load the rows and update the render and
453    /// syntax highlighting. If not found, do not return an error.
454    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        // If the file ends with an empty line or is empty, we need to append an empty
470        // row to `self.rows`. Unfortunately, BufReader::split doesn't yield an
471        // empty Vec in this case, so we need to check the last byte directly.
472        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        // The number of rows has changed. The left padding may need to be updated.
479        self.update_screen_cols();
480        self.n_bytes = self.rows.iter().map(|row| row.chars.len() as u64).sum();
481        Ok(())
482    }
483
484    /// Save the text to a file, given its name.
485    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    /// Save the text to a file and handle all errors. Errors and success
501    /// messages will be printed to the status bar. Return whether the file
502    /// was successfully saved.
503    fn save_and_handle_io_errors(&mut self, file_name: &str) -> bool {
504        let saved = self.save(file_name);
505        // Print error or success message to the status bar
506        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        // If save was successful, set dirty to false.
511        self.dirty &= saved.is_err();
512        saved.is_ok()
513    }
514
515    /// Save to a file after obtaining the file path from the prompt. If
516    /// successful, the `file_name` attribute of the editor will be set and
517    /// syntax highlighting will be updated.
518    fn save_as(&mut self, file_name: String) {
519        if self.save_and_handle_io_errors(&file_name) {
520            // If save was successful
521            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    /// Draw the left part of the screen: line numbers and vertical bar.
528    fn draw_left_padding<T: Display>(&self, buffer: &mut String, val: T) {
529        if self.ln_pad >= 2 {
530            // \u{2502}: pipe "│"
531            let s = format!("{:>1$} \u{2502}", val, self.ln_pad - 2);
532            // \x1b[38;5;240m: Dark grey color
533            push_colored(buffer, "\x1b[38;5;240m", &s, self.use_color);
534        }
535    }
536
537    /// Return whether the file being edited is empty or not. If there is more
538    /// than one row, even if all the rows are empty, `is_empty` returns
539    /// `false`, since the text contains new lines.
540    const fn is_empty(&self) -> bool { self.rows.len() <= 1 && self.n_bytes == 0 }
541
542    /// Draw rows of text and empty rows on the terminal, by adding characters
543    /// to the buffer.
544    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                // Draw a row of text
550                self.draw_left_padding(buffer, i + 1);
551                row.draw(self.cursor.coff, self.screen_cols, buffer, self.use_color);
552            } else {
553                // Draw an empty row
554                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    /// Draw the status bar on the terminal, by adding characters to the buffer.
565    fn draw_status_bar(&self, buffer: &mut String) {
566        // Left part of the status bar
567        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        // Right part of the status bar
573        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        // Draw
578        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    /// Draw the message bar on the terminal, by adding characters to the
583    /// buffer.
584    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    /// Refresh the screen: update the offsets, draw the rows, the status bar,
593    /// the message bar, and move the cursor to the correct position.
594    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            // If not in prompt mode, position the cursor according to the `cursor`
602            // attributes.
603            (self.rx() - self.cursor.coff + 1 + self.ln_pad, self.cursor.y - self.cursor.roff + 1)
604        } else {
605            // If in prompt mode, position the cursor on the prompt line at the end of the
606            // line.
607            (self.status_msg.as_ref().map_or(0, |sm| sm.msg.len() + 1), self.screen_rows + 2)
608        };
609        // Finally, print `buffer` and move the cursor
610        print!("{buffer}\x1b[{cursor_y};{cursor_x}H{SHOW_CURSOR}");
611        io::stdout().flush().map_err(Error::from)
612    }
613
614    /// Process a key that has been pressed, when not in prompt mode. Returns
615    /// whether the program should exit, and optionally the prompt mode to
616    /// switch to.
617    fn process_keypress(&mut self, key: &Key) -> (bool, Option<PromptMode>) {
618        // This won't be mutated, unless key is Key::Character(EXIT)
619        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(), // Enter
636            Key::Char(BACKSPACE | DELETE_BIS) => self.delete_char(), // Backspace or Ctrl + H
637            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                // TODO: Can we avoid using take() then reassigning the value to file_name?
653                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    /// Try to find a query, this is called after pressing Ctrl-F and for each
678    /// key that is pressed. `last_match` is the last row that was matched,
679    /// `forward` indicates whether to search forward or backward. Returns
680    /// the row of a new match, or `None` if the search was unsuccessful.
681    fn find(&mut self, query: &str, last_match: Option<usize>, forward: bool) -> Option<usize> {
682        // Number of rows to search
683        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        // TODO: Handle multiple matches per line
686        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.coff: Try to reset the column offset; if the match is after the
691                // offset, this will be updated in self.cursor.scroll() so that
692                // the result is visible
693                (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    /// If `file_name` is not None, load the file. Then run the text editor.
703    ///
704    /// # Errors
705    ///
706    /// Will Return `Err` if any error occur.
707    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            // TODO: Can we avoid using take()?
726            self.prompt_mode = match self.prompt_mode.take() {
727                // process_keypress returns (should_quit, prompt_mode)
728                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
738/// Set up the terminal and run the text editor. If `file_name` is not None,
739/// load the file.
740///
741/// Update the panic hook to restore the terminal on panic.
742///
743/// # Errors
744///
745/// Will Return `Err` if any error occur when registering the window size signal
746/// handler, enabling raw mode, or running the editor.
747pub 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    // Restore the original terminal mode.
764    terminal::restore_terminal(&orig_term_mode)?;
765
766    result
767}
768
769/// The prompt mode.
770#[cfg_attr(test, derive(Debug, PartialEq))]
771enum PromptMode {
772    /// Save(prompt buffer)
773    Save(String),
774    /// Find(prompt buffer, saved cursor state, last match)
775    Find(String, CursorState, Option<usize>),
776    /// GoTo(prompt buffer)
777    GoTo(String),
778    /// Execute(prompt buffer)
779    Execute(String),
780}
781
782// TODO: Use trait with mode_status_msg and process_keypress, implement the
783// trait for separate  structs for Save and Find?
784impl PromptMode {
785    /// Return the status message to print for the selected `PromptMode`.
786    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    /// Process a keypress event for the selected `PromptMode`.
796    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                    // The prompt was cancelled. Restore the previous position.
821                    PromptState::Cancelled => ed.cursor = saved_cursor,
822                    // Cursor has already been moved, do nothing
823                    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                        // saturating_sub: Lines and cols are 1-indexed
832                        .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/// The state of the prompt after processing a keypress event.
869#[cfg_attr(test, derive(Debug, PartialEq))]
870enum PromptState {
871    // Active contains the current buffer
872    Active(String),
873    // Completed contains the final string
874    Completed(String),
875    Cancelled,
876}
877
878/// Process a prompt keypress event and return the new state for the prompt.
879fn 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        // No-op
887        _ => (),
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    // round down!
941    #[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        // check current position
1031        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        // check current position
1075        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        // check current position
1099        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        // check current position
1136        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        // check current position
1179        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        // check current position
1210        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        // Set up Python syntax configuration for testing
1481        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        // Test commenting a line
1492        editor.cursor.y = 0; // First line
1493        editor.cursor.x = 0;
1494        editor.process_keypress(&Key::Char(TOGGLE_COMMENT));
1495        assert_eq!(editor.rows[0].chars, b"# def hello():");
1496
1497        // Test uncommenting the same line
1498        editor.process_keypress(&Key::Char(TOGGLE_COMMENT));
1499        assert_eq!(editor.rows[0].chars, b"def hello():");
1500
1501        // Test commenting an indented line
1502        editor.cursor.y = 1; // Second line (indented)
1503        editor.cursor.x = 0;
1504        editor.process_keypress(&Key::Char(TOGGLE_COMMENT));
1505        assert_eq!(editor.rows[1].chars, b"    # print(\"Hello\")");
1506
1507        // Test uncommenting the indented line
1508        editor.process_keypress(&Key::Char(TOGGLE_COMMENT));
1509        assert_eq!(editor.rows[1].chars, b"    print(\"Hello\")");
1510
1511        // Test the bug case: cursor at end of line during toggle
1512        editor.cursor.y = 0; // First line
1513        editor.cursor.x = editor.rows[0].chars.len(); // Position at end
1514        editor.process_keypress(&Key::Char(TOGGLE_COMMENT)); // Comment
1515        assert_eq!(editor.rows[0].chars, b"# def hello():");
1516
1517        // Now uncomment with cursor still at end - this should not panic
1518        editor.cursor.x = editor.rows[0].chars.len(); // Position at end again
1519        editor.process_keypress(&Key::Char(TOGGLE_COMMENT)); // Uncomment
1520        assert_eq!(editor.rows[0].chars, b"def hello():");
1521
1522        // Verify cursor position is valid
1523        assert!(editor.cursor.x <= editor.rows[0].chars.len());
1524    }
1525}