• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ilai-deutel / kibi / 19286649571

12 Nov 2025 04:41AM UTC coverage: 62.38% (-0.9%) from 63.31%
19286649571

push

github

ilai-deutel
Set up panic hook to restore terminal mode on panic (#496)

8 of 21 new or added lines in 4 files covered. (38.1%)

167 existing lines in 3 files now uncovered.

713 of 1143 relevant lines covered (62.38%)

705.97 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

58.99
/src/editor.rs
1
use std::fmt::{Display, Write as _};
2
use std::io::{self, BufRead, BufReader, ErrorKind, Read, Seek, Write};
3
use std::iter::{self, repeat, successors};
4
use std::{fs::File, path::Path, process::Command, time::Instant};
5

6
use crate::row::{HlState, Row};
7
use crate::{Config, Error, ansi_escape::*, syntax::Conf as SyntaxConf, sys, terminal};
8

UNCOV
9
const fn ctrl_key(key: u8) -> u8 { key & 0x1f }
×
10
const EXIT: u8 = ctrl_key(b'Q');
11
const DELETE_BIS: u8 = ctrl_key(b'H');
12
const REFRESH_SCREEN: u8 = ctrl_key(b'L');
13
const SAVE: u8 = ctrl_key(b'S');
14
const FIND: u8 = ctrl_key(b'F');
15
const GOTO: u8 = ctrl_key(b'G');
16
const CUT: u8 = ctrl_key(b'X');
17
const COPY: u8 = ctrl_key(b'C');
18
const PASTE: u8 = ctrl_key(b'V');
19
const DUPLICATE: u8 = ctrl_key(b'D');
20
const EXECUTE: u8 = ctrl_key(b'E');
21
const REMOVE_LINE: u8 = ctrl_key(b'R');
22
const BACKSPACE: u8 = 127;
23

24
const HELP_MESSAGE: &str = "^S save | ^Q quit | ^F find | ^G go to | ^D duplicate | ^E execute | \
25
                            ^C copy | ^X cut | ^V paste";
26

27
/// `set_status!` sets a formatted status message for the editor.
28
/// Example usage: `set_status!(editor, "{file_size} written to {file_name}")`
29
macro_rules! set_status { ($editor:expr, $($arg:expr),*) => ($editor.status_msg = Some(StatusMessage::new(format!($($arg),*)))) }
30

31
/// Enum of input keys
32
#[cfg_attr(test, derive(Debug, PartialEq))]
33
enum Key {
34
    Arrow(AKey),
35
    CtrlArrow(AKey),
36
    PageUp,
37
    PageDown,
38
    Home,
39
    End,
40
    Delete,
41
    Escape,
42
    Char(u8),
43
}
44

45
/// Enum of arrow keys
46
#[cfg_attr(test, derive(Debug, PartialEq))]
47
enum AKey {
48
    Left,
49
    Right,
50
    Up,
51
    Down,
52
}
53

54
/// Describes the cursor position and the screen offset
55
#[derive(Debug, Default, Clone, PartialEq)]
56
struct CursorState {
57
    /// x position (indexing the characters, not the columns)
58
    x: usize,
59
    /// y position (row number, 0-indexed)
60
    y: usize,
61
    /// Row offset
62
    roff: usize,
63
    /// Column offset
64
    coff: usize,
65
}
66

67
impl CursorState {
68
    const fn move_to_next_line(&mut self) { (self.x, self.y) = (0, self.y + 1); }
242✔
69

70
    /// Scroll the terminal window vertically and horizontally (i.e. adjusting
71
    /// the row offset and the column offset) so that the cursor can be
72
    /// shown.
UNCOV
73
    fn scroll(&mut self, rx: usize, screen_rows: usize, screen_cols: usize) {
×
UNCOV
74
        self.roff = self.roff.clamp(self.y.saturating_sub(screen_rows.saturating_sub(1)), self.y);
×
UNCOV
75
        self.coff = self.coff.clamp(rx.saturating_sub(screen_cols.saturating_sub(1)), rx);
×
UNCOV
76
    }
×
77
}
78

79
/// The `Editor` struct, contains the state and configuration of the text
80
/// editor.
81
#[derive(Default)]
82
pub struct Editor {
83
    /// If not `None`, the current prompt mode (`Save`, `Find`, `GoTo`, or
84
    /// `Execute`). If `None`, we are in regular edition mode.
85
    prompt_mode: Option<PromptMode>,
86
    /// The current state of the cursor.
87
    cursor: CursorState,
88
    /// The padding size used on the left for line numbering.
89
    ln_pad: usize,
90
    /// The width of the current window. Will be updated when the window is
91
    /// resized.
92
    window_width: usize,
93
    /// The number of rows that can be used for the editor, excluding the status
94
    /// bar and the message bar
95
    screen_rows: usize,
96
    /// The number of columns that can be used for the editor, excluding the
97
    /// part used for line numbers
98
    screen_cols: usize,
99
    /// The collection of rows, including the content and the syntax
100
    /// highlighting information.
101
    rows: Vec<Row>,
102
    /// Whether the document has been modified since it was open.
103
    dirty: bool,
104
    /// The configuration for the editor.
105
    config: Config,
106
    /// The number of consecutive times the user has tried to quit without
107
    /// saving. After `config.quit_times`, the program will exit.
108
    quit_times: usize,
109
    /// The file name. If None, the user will be prompted for a file name the
110
    /// first time they try to save.
111
    // TODO: It may be better to store a PathBuf instead
112
    file_name: Option<String>,
113
    /// The current status message being shown.
114
    status_msg: Option<StatusMessage>,
115
    /// The syntax configuration corresponding to the current file's extension.
116
    syntax: SyntaxConf,
117
    /// The number of bytes contained in `rows`. This excludes new lines.
118
    n_bytes: u64,
119
    /// The copied buffer of a row
120
    copied_row: Vec<u8>,
121
}
122

123
/// Describes a status message, shown at the bottom at the screen.
124
struct StatusMessage {
125
    /// The message to display.
126
    msg: String,
127
    /// The `Instant` the status message was first displayed.
128
    time: Instant,
129
}
130

131
impl StatusMessage {
132
    /// Create a new status message and set time to the current date/time.
UNCOV
133
    fn new(msg: String) -> Self { Self { msg, time: Instant::now() } }
×
134
}
135

136
/// Pretty-format a size in bytes.
137
fn format_size(n: u64) -> String {
187✔
138
    if n < 1024 {
187✔
139
        return format!("{n}B");
33✔
140
    }
154✔
141
    // i is the largest value such that 1024 ^ i < n
142
    let i = n.ilog2() / 10;
154✔
143

144
    // Compute the size with two decimal places (rounded down) as the last two
145
    // digits of q This avoid float formatting reducing the binary size
146
    let q = 100 * n / (1024 << ((i - 1) * 10));
154✔
147
    format!("{}.{:02}{}B", q / 100, q % 100, b" kMGTPEZ"[i as usize] as char)
154✔
148
}
187✔
149

150
/// Return an Arrow Key given an ANSI code.
151
///
152
/// The argument must be a valide arrow key ANSI code (`a`, `b`, `c` or `d`),
153
/// case-insensitive).
154
fn get_akey(c: u8) -> AKey {
66✔
155
    match c {
66✔
156
        b'a' | b'A' => AKey::Up,
11✔
157
        b'b' | b'B' => AKey::Down,
11✔
158
        b'c' | b'C' => AKey::Right,
33✔
159
        b'd' | b'D' => AKey::Left,
11✔
UNCOV
160
        _ => unreachable!("Invalid ANSI code for arrow key {}", c),
×
161
    }
162
}
66✔
163

164
impl Editor {
165
    /// Return the current row if the cursor points to an existing row, `None`
166
    /// otherwise.
167
    fn current_row(&self) -> Option<&Row> { self.rows.get(self.cursor.y) }
1,012✔
168

169
    /// Return the position of the cursor, in terms of rendered characters (as
170
    /// opposed to `self.cursor.x`, which is the position of the cursor in
171
    /// terms of bytes).
UNCOV
172
    fn rx(&self) -> usize { self.current_row().map_or(0, |r| r.cx2rx[self.cursor.x]) }
×
173

174
    /// Move the cursor following an arrow key (← → ↑ ↓).
175
    fn move_cursor(&mut self, key: &AKey, ctrl: bool) {
495✔
176
        match (key, self.current_row()) {
495✔
177
            (AKey::Left, Some(row)) if self.cursor.x > 0 => {
165✔
178
                let mut cursor_x = self.cursor.x - row.get_char_size(row.cx2rx[self.cursor.x] - 1);
132✔
179
                // ← moving to previous word
180
                while ctrl && cursor_x > 0 && row.chars[cursor_x - 1] != b' ' {
539✔
181
                    cursor_x -= row.get_char_size(row.cx2rx[cursor_x] - 1);
407✔
182
                }
407✔
183
                self.cursor.x = cursor_x;
132✔
184
            }
185
            // ← at the beginning of the line: move to the end of the previous line. The x
186
            // position will be adjusted after this `match` to accommodate the current row
187
            // length, so we can just set here to the maximum possible value here.
188
            (AKey::Left, _) if self.cursor.y > 0 =>
44✔
189
                (self.cursor.y, self.cursor.x) = (self.cursor.y - 1, usize::MAX),
33✔
190
            (AKey::Right, Some(row)) if self.cursor.x < row.chars.len() => {
99✔
191
                let mut cursor_x = self.cursor.x + row.get_char_size(row.cx2rx[self.cursor.x]);
55✔
192
                // → moving to next word
193
                while ctrl && cursor_x < row.chars.len() && row.chars[cursor_x] != b' ' {
275✔
194
                    cursor_x += row.get_char_size(row.cx2rx[cursor_x]);
220✔
195
                }
220✔
196
                self.cursor.x = cursor_x;
55✔
197
            }
198
            (AKey::Right, Some(_)) => self.cursor.move_to_next_line(),
44✔
199
            // TODO: For Up and Down, move self.cursor.x to be consistent with tabs and UTF-8
200
            //  characters, i.e. according to rx
201
            (AKey::Up, _) if self.cursor.y > 0 => self.cursor.y -= 1,
154✔
202
            (AKey::Down, Some(_)) => self.cursor.y += 1,
44✔
203
            _ => (),
44✔
204
        }
205
        self.update_cursor_x_position();
495✔
206
    }
495✔
207

208
    /// Update the cursor x position. If the cursor y position has changed, the
209
    /// current position might be illegal (x is further right than the last
210
    /// character of the row). If that is the case, clamp `self.cursor.x`.
211
    fn update_cursor_x_position(&mut self) {
495✔
212
        self.cursor.x = self.cursor.x.min(self.current_row().map_or(0, |row| row.chars.len()));
495✔
213
    }
495✔
214

215
    /// Run a loop to obtain the key that was pressed. At each iteration of the
216
    /// loop (until a key is pressed), we listen to the `ws_changed` channel
217
    /// to check if a window size change signal has been received. When
218
    /// bytes are received, we match to a corresponding `Key`. In particular,
219
    /// we handle ANSI escape codes to return `Key::Delete`, `Key::Home` etc.
220
    fn loop_until_keypress(&mut self, input: &mut impl BufRead) -> Result<Key, Error> {
154✔
221
        let mut bytes = input.bytes();
154✔
222
        loop {
223
            // Handle window size if a signal has be received
224
            if sys::has_window_size_changed() {
154✔
UNCOV
225
                self.update_window_size()?;
×
UNCOV
226
                self.refresh_screen()?;
×
227
            }
154✔
228
            if let Some(a) = bytes.next().transpose()? {
154✔
229
                // Match on the next byte received or, if the first byte is <ESC> ('\x1b'), on
230
                // the next few bytes.
231
                if a != b'\x1b' {
154✔
232
                    return Ok(Key::Char(a));
33✔
233
                }
121✔
234
                return Ok(match bytes.next().transpose()? {
121✔
235
                    Some(b @ (b'[' | b'O')) => match (b, bytes.next().transpose()?) {
121✔
236
                        (b'[', Some(c @ b'A'..=b'D')) => Key::Arrow(get_akey(c)),
66✔
237
                        (b'[' | b'O', Some(b'H')) => Key::Home,
22✔
238
                        (b'[' | b'O', Some(b'F')) => Key::End,
22✔
239
                        (b'[', mut c @ Some(b'0'..=b'8')) => {
33✔
240
                            let mut d = bytes.next().transpose()?;
22✔
241
                            if (c, d) == (Some(b'1'), Some(b';')) {
22✔
242
                                // 1 is the default modifier value. Therefore, <ESC>[1;5C is
243
                                // equivalent to <ESC>[5C, etc.
244
                                c = bytes.next().transpose()?;
11✔
245
                                d = bytes.next().transpose()?;
11✔
246
                            }
11✔
247
                            match (c, d) {
22✔
UNCOV
248
                                (Some(c), Some(b'~')) if c == b'1' || c == b'7' => Key::Home,
×
UNCOV
249
                                (Some(c), Some(b'~')) if c == b'4' || c == b'8' => Key::End,
×
UNCOV
250
                                (Some(b'3'), Some(b'~')) => Key::Delete,
×
UNCOV
251
                                (Some(b'5'), Some(b'~')) => Key::PageUp,
×
UNCOV
252
                                (Some(b'6'), Some(b'~')) => Key::PageDown,
×
253
                                (Some(b'5'), Some(d @ b'A'..=b'D')) => Key::CtrlArrow(get_akey(d)),
22✔
254
                                _ => Key::Escape,
×
255
                            }
256
                        }
UNCOV
257
                        (b'O', Some(c @ b'a'..=b'd')) => Key::CtrlArrow(get_akey(c)),
×
258
                        _ => Key::Escape,
11✔
259
                    },
UNCOV
260
                    _ => Key::Escape,
×
261
                });
UNCOV
262
            }
×
263
        }
264
    }
154✔
265

266
    /// Update the `screen_rows`, `window_width`, `screen_cols` and `ln_padding`
267
    /// attributes.
UNCOV
268
    fn update_window_size(&mut self) -> Result<(), Error> {
×
UNCOV
269
        let wsize = sys::get_window_size().or_else(|_| terminal::get_window_size_using_cursor())?;
×
270
        // Make room for the status bar and status message
UNCOV
271
        (self.screen_rows, self.window_width) = (wsize.0.saturating_sub(2), wsize.1);
×
UNCOV
272
        self.update_screen_cols();
×
UNCOV
273
        Ok(())
×
UNCOV
274
    }
×
275

276
    /// Update the `screen_cols` and `ln_padding` attributes based on the
277
    /// maximum number of digits for line numbers (since the left padding
278
    /// depends on this number of digits).
279
    fn update_screen_cols(&mut self) {
308✔
280
        // The maximum number of digits to use for the line number is the number of
281
        // digits of the last line number. This is equal to the number of times
282
        // we can divide this number by ten, computed below using `successors`.
283
        let n_digits =
308✔
284
            successors(Some(self.rows.len()), |u| Some(u / 10).filter(|u| *u > 0)).count();
308✔
285
        let show_line_num = self.config.show_line_num && n_digits + 2 < self.window_width / 4;
308✔
286
        self.ln_pad = if show_line_num { n_digits + 2 } else { 0 };
308✔
287
        self.screen_cols = self.window_width.saturating_sub(self.ln_pad);
308✔
288
    }
308✔
289

290
    /// Update a row, given its index. If `ignore_following_rows` is `false` and
291
    /// the highlight state has changed during the update (for instance, it
292
    /// is now in "multi-line comment" state, keep updating the next rows
293
    fn update_row(&mut self, y: usize, ignore_following_rows: bool) {
2,442✔
294
        let mut hl_state = if y > 0 { self.rows[y - 1].hl_state } else { HlState::Normal };
2,442✔
295
        for row in self.rows.iter_mut().skip(y) {
2,442✔
296
            let previous_hl_state = row.hl_state;
2,431✔
297
            hl_state = row.update(&self.syntax, hl_state, self.config.tab_stop);
2,431✔
298
            if ignore_following_rows || hl_state == previous_hl_state {
2,431✔
299
                return;
2,431✔
300
            }
×
301
            // If the state has changed (for instance, a multi-line comment
302
            // started in this row), continue updating the following
303
            // rows
304
        }
305
    }
2,442✔
306

307
    /// Update all the rows.
UNCOV
308
    fn update_all_rows(&mut self) {
×
UNCOV
309
        let mut hl_state = HlState::Normal;
×
UNCOV
310
        for row in &mut self.rows {
×
UNCOV
311
            hl_state = row.update(&self.syntax, hl_state, self.config.tab_stop);
×
UNCOV
312
        }
×
UNCOV
313
    }
×
314

315
    /// Insert a byte at the current cursor position. If there is no row at the
316
    /// current cursor position, add a new row and insert the byte.
317
    fn insert_byte(&mut self, c: u8) {
2,046✔
318
        if let Some(row) = self.rows.get_mut(self.cursor.y) {
2,046✔
319
            row.chars.insert(self.cursor.x, c);
1,947✔
320
        } else {
1,947✔
321
            self.rows.push(Row::new(vec![c]));
99✔
322
            // The number of rows has changed. The left padding may need to be updated.
99✔
323
            self.update_screen_cols();
99✔
324
        }
99✔
325
        self.update_row(self.cursor.y, false);
2,046✔
326
        (self.cursor.x, self.n_bytes, self.dirty) = (self.cursor.x + 1, self.n_bytes + 1, true);
2,046✔
327
    }
2,046✔
328

329
    /// Insert a new line at the current cursor position and move the cursor to
330
    /// the start of the new line. If the cursor is in the middle of a row,
331
    /// split off that row.
332
    fn insert_new_line(&mut self) {
198✔
333
        let (position, new_row_chars) = if self.cursor.x == 0 {
198✔
334
            (self.cursor.y, Vec::new())
55✔
335
        } else {
336
            // self.rows[self.cursor.y] must exist, since cursor.x = 0 for any cursor.y ≥
337
            // row.len()
338
            let new_chars = self.rows[self.cursor.y].chars.split_off(self.cursor.x);
143✔
339
            self.update_row(self.cursor.y, false);
143✔
340
            (self.cursor.y + 1, new_chars)
143✔
341
        };
342
        self.rows.insert(position, Row::new(new_row_chars));
198✔
343
        self.update_row(position, false);
198✔
344
        self.update_screen_cols();
198✔
345
        self.cursor.move_to_next_line();
198✔
346
        self.dirty = true;
198✔
347
    }
198✔
348

349
    /// Delete a character at the current cursor position. If the cursor is
350
    /// located at the beginning of a row that is not the first or last row,
351
    /// merge the current row and the previous row. If the cursor is located
352
    /// after the last row, move up to the last character of the previous row.
353
    fn delete_char(&mut self) {
55✔
354
        if self.cursor.x > 0 {
55✔
355
            let row = &mut self.rows[self.cursor.y];
33✔
356
            // Obtain the number of bytes to be removed: could be 1-4 (UTF-8 character
357
            // size).
358
            let n_bytes_to_remove = row.get_char_size(row.cx2rx[self.cursor.x] - 1);
33✔
359
            row.chars.splice(self.cursor.x - n_bytes_to_remove..self.cursor.x, iter::empty());
33✔
360
            self.update_row(self.cursor.y, false);
33✔
361
            self.cursor.x -= n_bytes_to_remove;
33✔
362
            self.dirty = if self.is_empty() { self.file_name.is_some() } else { true };
33✔
363
            self.n_bytes -= n_bytes_to_remove as u64;
33✔
364
        } else if self.cursor.y < self.rows.len() && self.cursor.y > 0 {
22✔
365
            let row = self.rows.remove(self.cursor.y);
11✔
366
            let previous_row = &mut self.rows[self.cursor.y - 1];
11✔
367
            self.cursor.x = previous_row.chars.len();
11✔
368
            previous_row.chars.extend(&row.chars);
11✔
369
            self.update_row(self.cursor.y - 1, true);
11✔
370
            self.update_row(self.cursor.y, false);
11✔
371
            // The number of rows has changed. The left padding may need to be updated.
11✔
372
            self.update_screen_cols();
11✔
373
            (self.dirty, self.cursor.y) = (self.dirty, self.cursor.y - 1);
11✔
374
        } else if self.cursor.y == self.rows.len() {
11✔
375
            // If the cursor is located after the last row, pressing backspace is equivalent
11✔
376
            // to pressing the left arrow key.
11✔
377
            self.move_cursor(&AKey::Left, false);
11✔
378
        }
11✔
379
    }
55✔
380

UNCOV
381
    fn delete_current_row(&mut self) {
×
UNCOV
382
        if self.cursor.y < self.rows.len() {
×
UNCOV
383
            self.rows[self.cursor.y].chars.clear();
×
UNCOV
384
            self.update_row(self.cursor.y, false);
×
UNCOV
385
            self.cursor.move_to_next_line();
×
UNCOV
386
            self.delete_char();
×
UNCOV
387
        }
×
UNCOV
388
    }
×
389

UNCOV
390
    fn duplicate_current_row(&mut self) {
×
UNCOV
391
        self.copy_current_row();
×
UNCOV
392
        self.paste_current_row();
×
UNCOV
393
    }
×
394

UNCOV
395
    fn copy_current_row(&mut self) {
×
UNCOV
396
        if let Some(row) = self.current_row() {
×
UNCOV
397
            self.copied_row = row.chars.clone();
×
UNCOV
398
        }
×
UNCOV
399
    }
×
400

UNCOV
401
    fn paste_current_row(&mut self) {
×
UNCOV
402
        if self.copied_row.is_empty() {
×
UNCOV
403
            return;
×
UNCOV
404
        }
×
UNCOV
405
        self.n_bytes += self.copied_row.len() as u64;
×
UNCOV
406
        if self.cursor.y == self.rows.len() {
×
UNCOV
407
            self.rows.push(Row::new(self.copied_row.clone()));
×
UNCOV
408
        } else {
×
409
            self.rows.insert(self.cursor.y + 1, Row::new(self.copied_row.clone()));
×
410
        }
×
411
        self.update_row(self.cursor.y + usize::from(self.cursor.y + 1 != self.rows.len()), false);
×
412
        (self.cursor.y, self.dirty) = (self.cursor.y + 1, true);
×
413
        // The line number has changed
414
        self.update_screen_cols();
×
415
    }
×
416

417
    /// Try to load a file. If found, load the rows and update the render and
418
    /// syntax highlighting. If not found, do not return an error.
419
    fn load(&mut self, path: &Path) -> Result<(), Error> {
×
420
        let mut file = match File::open(path) {
×
421
            Err(e) if e.kind() == ErrorKind::NotFound => {
×
UNCOV
422
                self.rows.push(Row::new(Vec::new()));
×
423
                return Ok(());
×
424
            }
425
            r => r,
×
426
        }?;
×
427
        let ft = file.metadata()?.file_type();
×
UNCOV
428
        if !(ft.is_file() || ft.is_symlink()) {
×
429
            return Err(io::Error::new(ErrorKind::InvalidInput, "Invalid input file type").into());
×
430
        }
×
431
        for line in BufReader::new(&file).split(b'\n') {
×
432
            self.rows.push(Row::new(line?));
×
433
        }
434
        // If the file ends with an empty line or is empty, we need to append an empty
435
        // row to `self.rows`. Unfortunately, BufReader::split doesn't yield an
436
        // empty Vec in this case, so we need to check the last byte directly.
437
        file.seek(io::SeekFrom::End(0))?;
×
438
        #[expect(clippy::unbuffered_bytes)]
439
        if file.bytes().next().transpose()?.is_none_or(|b| b == b'\n') {
×
440
            self.rows.push(Row::new(Vec::new()));
×
UNCOV
441
        }
×
442
        self.update_all_rows();
×
443
        // The number of rows has changed. The left padding may need to be updated.
UNCOV
444
        self.update_screen_cols();
×
UNCOV
445
        self.n_bytes = self.rows.iter().map(|row| row.chars.len() as u64).sum();
×
UNCOV
446
        Ok(())
×
447
    }
×
448

449
    /// Save the text to a file, given its name.
450
    fn save(&self, file_name: &str) -> Result<usize, io::Error> {
×
451
        let mut file = File::create(file_name)?;
×
UNCOV
452
        let mut written = 0;
×
453
        for (i, row) in self.rows.iter().enumerate() {
×
454
            file.write_all(&row.chars)?;
×
455
            written += row.chars.len();
×
456
            if i != (self.rows.len() - 1) {
×
457
                file.write_all(b"\n")?;
×
458
                written += 1;
×
459
            }
×
460
        }
UNCOV
461
        file.sync_all()?;
×
UNCOV
462
        Ok(written)
×
UNCOV
463
    }
×
464

465
    /// Save the text to a file and handle all errors. Errors and success
466
    /// messages will be printed to the status bar. Return whether the file
467
    /// was successfully saved.
468
    fn save_and_handle_io_errors(&mut self, file_name: &str) -> bool {
×
469
        let saved = self.save(file_name);
×
470
        // Print error or success message to the status bar
UNCOV
471
        match saved.as_ref() {
×
472
            Ok(w) => set_status!(self, "{} written to {}", format_size(*w as u64), file_name),
×
473
            Err(err) => set_status!(self, "Can't save! I/O error: {err}"),
×
474
        }
475
        // If save was successful, set dirty to false.
UNCOV
476
        self.dirty &= saved.is_err();
×
UNCOV
477
        saved.is_ok()
×
478
    }
×
479

480
    /// Save to a file after obtaining the file path from the prompt. If
481
    /// successful, the `file_name` attribute of the editor will be set and
482
    /// syntax highlighting will be updated.
483
    fn save_as(&mut self, file_name: String) {
×
484
        if self.save_and_handle_io_errors(&file_name) {
×
485
            // If save was successful
×
486
            self.syntax = SyntaxConf::find(&file_name, &sys::data_dirs());
×
487
            self.file_name = Some(file_name);
×
UNCOV
488
            self.update_all_rows();
×
489
        }
×
490
    }
×
491

492
    /// Draw the left part of the screen: line numbers and vertical bar.
UNCOV
493
    fn draw_left_padding<T: Display>(&self, buffer: &mut String, val: T) -> Result<(), Error> {
×
UNCOV
494
        if self.ln_pad >= 2 {
×
495
            // \x1b[38;5;240m: Dark grey color; \u{2502}: pipe "│"
496
            write!(buffer, "\x1b[38;5;240m{:>2$} \u{2502}{}", val, RESET_FMT, self.ln_pad - 2)?;
×
497
        }
×
UNCOV
498
        Ok(())
×
499
    }
×
500

501
    /// Return whether the file being edited is empty or not. If there is more
502
    /// than one row, even if all the rows are empty, `is_empty` returns
503
    /// `false`, since the text contains new lines.
504
    fn is_empty(&self) -> bool { self.rows.len() <= 1 && self.n_bytes == 0 }
33✔
505

506
    /// Draw rows of text and empty rows on the terminal, by adding characters
507
    /// to the buffer.
UNCOV
508
    fn draw_rows(&self, buffer: &mut String) -> Result<(), Error> {
×
UNCOV
509
        let row_it = self.rows.iter().map(Some).chain(repeat(None)).enumerate();
×
UNCOV
510
        for (i, row) in row_it.skip(self.cursor.roff).take(self.screen_rows) {
×
511
            buffer.push_str(CLEAR_LINE_RIGHT_OF_CURSOR);
×
512
            if let Some(row) = row {
×
513
                // Draw a row of text
514
                self.draw_left_padding(buffer, i + 1)?;
×
515
                row.draw(self.cursor.coff, self.screen_cols, buffer)?;
×
516
            } else {
517
                // Draw an empty row
518
                self.draw_left_padding(buffer, '~')?;
×
UNCOV
519
                if self.is_empty() && i == self.screen_rows / 3 {
×
UNCOV
520
                    let welcome_message = concat!("Kibi ", env!("KIBI_VERSION"));
×
521
                    write!(buffer, "{:^1$.1$}", welcome_message, self.screen_cols)?;
×
522
                }
×
523
            }
524
            buffer.push_str("\r\n");
×
525
        }
526
        Ok(())
×
527
    }
×
528

529
    /// Draw the status bar on the terminal, by adding characters to the buffer.
UNCOV
530
    fn draw_status_bar(&self, buffer: &mut String) -> Result<(), Error> {
×
531
        // Left part of the status bar
UNCOV
532
        let modified = if self.dirty { " (modified)" } else { "" };
×
UNCOV
533
        let mut left =
×
UNCOV
534
            format!("{:.30}{modified}", self.file_name.as_deref().unwrap_or("[No Name]"));
×
UNCOV
535
        left.truncate(self.window_width);
×
536

537
        // Right part of the status bar
538
        let size = format_size(self.n_bytes + self.rows.len().saturating_sub(1) as u64);
×
539
        let right =
×
540
            format!("{} | {size} | {}:{}", self.syntax.name, self.cursor.y + 1, self.rx() + 1);
×
541

542
        // Draw
543
        let rw = self.window_width.saturating_sub(left.len());
×
UNCOV
544
        write!(buffer, "{REVERSE_VIDEO}{left}{right:>rw$.rw$}{RESET_FMT}\r\n")?;
×
UNCOV
545
        Ok(())
×
546
    }
×
547

548
    /// Draw the message bar on the terminal, by adding characters to the
549
    /// buffer.
550
    fn draw_message_bar(&self, buffer: &mut String) {
×
UNCOV
551
        buffer.push_str(CLEAR_LINE_RIGHT_OF_CURSOR);
×
552
        let msg_duration = self.config.message_dur;
×
UNCOV
553
        if let Some(sm) = self.status_msg.as_ref().filter(|sm| sm.time.elapsed() < msg_duration) {
×
554
            buffer.push_str(&sm.msg[..sm.msg.len().min(self.window_width)]);
×
555
        }
×
UNCOV
556
    }
×
557

558
    /// Refresh the screen: update the offsets, draw the rows, the status bar,
559
    /// the message bar, and move the cursor to the correct position.
560
    fn refresh_screen(&mut self) -> Result<(), Error> {
×
561
        self.cursor.scroll(self.rx(), self.screen_rows, self.screen_cols);
×
562
        let mut buffer = format!("{HIDE_CURSOR}{MOVE_CURSOR_TO_START}");
×
563
        self.draw_rows(&mut buffer)?;
×
UNCOV
564
        self.draw_status_bar(&mut buffer)?;
×
UNCOV
565
        self.draw_message_bar(&mut buffer);
×
566
        let (cursor_x, cursor_y) = if self.prompt_mode.is_none() {
×
567
            // If not in prompt mode, position the cursor according to the `cursor`
568
            // attributes.
UNCOV
569
            (self.rx() - self.cursor.coff + 1 + self.ln_pad, self.cursor.y - self.cursor.roff + 1)
×
570
        } else {
571
            // If in prompt mode, position the cursor on the prompt line at the end of the
572
            // line.
573
            (self.status_msg.as_ref().map_or(0, |sm| sm.msg.len() + 1), self.screen_rows + 2)
×
574
        };
575
        // Finally, print `buffer` and move the cursor
UNCOV
576
        print!("{buffer}\x1b[{cursor_y};{cursor_x}H{SHOW_CURSOR}");
×
UNCOV
577
        io::stdout().flush().map_err(Error::from)
×
578
    }
×
579

580
    /// Process a key that has been pressed, when not in prompt mode. Returns
581
    /// whether the program should exit, and optionally the prompt mode to
582
    /// switch to.
583
    fn process_keypress(&mut self, key: &Key) -> (bool, Option<PromptMode>) {
2,101✔
584
        // This won't be mutated, unless key is Key::Character(EXIT)
585
        let mut reset_quit_times = true;
2,101✔
586
        let mut prompt_mode = None;
2,101✔
587

588
        match key {
2,101✔
589
            Key::Arrow(arrow) => self.move_cursor(arrow, false),
×
590
            Key::CtrlArrow(arrow) => self.move_cursor(arrow, true),
×
591
            Key::PageUp => {
×
592
                self.cursor.y = self.cursor.roff.saturating_sub(self.screen_rows);
×
593
                self.update_cursor_x_position();
×
594
            }
×
UNCOV
595
            Key::PageDown => {
×
UNCOV
596
                self.cursor.y = (self.cursor.roff + 2 * self.screen_rows - 1).min(self.rows.len());
×
597
                self.update_cursor_x_position();
×
UNCOV
598
            }
×
599
            Key::Home => self.cursor.x = 0,
22✔
600
            Key::End => self.cursor.x = self.current_row().map_or(0, |row| row.chars.len()),
22✔
601
            Key::Char(b'\r' | b'\n') => self.insert_new_line(), // Enter
143✔
UNCOV
602
            Key::Char(BACKSPACE | DELETE_BIS) => self.delete_char(), // Backspace or Ctrl + H
×
UNCOV
603
            Key::Char(REMOVE_LINE) => self.delete_current_row(),
×
604
            Key::Delete => {
33✔
605
                self.move_cursor(&AKey::Right, false);
33✔
606
                self.delete_char();
33✔
607
            }
33✔
UNCOV
608
            Key::Escape | Key::Char(REFRESH_SCREEN) => (),
×
609
            Key::Char(EXIT) => {
UNCOV
610
                if !self.dirty || self.quit_times + 1 >= self.config.quit_times {
×
UNCOV
611
                    return (true, None);
×
UNCOV
612
                }
×
UNCOV
613
                let r = self.config.quit_times - self.quit_times - 1;
×
UNCOV
614
                set_status!(self, "Press Ctrl+Q {0} more time{1:.2$} to quit.", r, "s", r - 1);
×
UNCOV
615
                reset_quit_times = false;
×
616
            }
617
            Key::Char(SAVE) => match self.file_name.take() {
×
618
                // TODO: Can we avoid using take() then reassigning the value to file_name?
619
                Some(file_name) => {
×
620
                    self.save_and_handle_io_errors(&file_name);
×
621
                    self.file_name = Some(file_name);
×
622
                }
×
623
                None => prompt_mode = Some(PromptMode::Save(String::new())),
×
624
            },
625
            Key::Char(FIND) =>
626
                prompt_mode = Some(PromptMode::Find(String::new(), self.cursor.clone(), None)),
×
UNCOV
627
            Key::Char(GOTO) => prompt_mode = Some(PromptMode::GoTo(String::new())),
×
UNCOV
628
            Key::Char(DUPLICATE) => self.duplicate_current_row(),
×
UNCOV
629
            Key::Char(CUT) => {
×
630
                self.copy_current_row();
×
631
                self.delete_current_row();
×
UNCOV
632
            }
×
UNCOV
633
            Key::Char(COPY) => self.copy_current_row(),
×
UNCOV
634
            Key::Char(PASTE) => self.paste_current_row(),
×
UNCOV
635
            Key::Char(EXECUTE) => prompt_mode = Some(PromptMode::Execute(String::new())),
×
636
            Key::Char(c) => self.insert_byte(*c),
1,881✔
637
        }
638
        self.quit_times = if reset_quit_times { 0 } else { self.quit_times + 1 };
2,101✔
639
        (false, prompt_mode)
2,101✔
640
    }
2,101✔
641

642
    /// Try to find a query, this is called after pressing Ctrl-F and for each
643
    /// key that is pressed. `last_match` is the last row that was matched,
644
    /// `forward` indicates whether to search forward or backward. Returns
645
    /// the row of a new match, or `None` if the search was unsuccessful.
646
    fn find(&mut self, query: &str, last_match: Option<usize>, forward: bool) -> Option<usize> {
132✔
647
        // Number of rows to search
648
        let num_rows = if query.is_empty() { 0 } else { self.rows.len() };
132✔
649
        let mut current = last_match.unwrap_or_else(|| num_rows.saturating_sub(1));
132✔
650
        // TODO: Handle multiple matches per line
651
        for _ in 0..num_rows {
132✔
652
            current = (current + if forward { 1 } else { num_rows - 1 }) % num_rows;
110✔
653
            let row = &mut self.rows[current];
110✔
654
            if let Some(cx) = row.chars.windows(query.len()).position(|w| w == query.as_bytes()) {
110✔
655
                // self.cursor.coff: Try to reset the column offset; if the match is after the
656
                // offset, this will be updated in self.cursor.scroll() so that
657
                // the result is visible
658
                (self.cursor.x, self.cursor.y, self.cursor.coff) = (cx, current, 0);
×
659
                let rx = row.cx2rx[cx];
×
660
                row.match_segment = Some(rx..rx + query.len());
×
661
                return Some(current);
×
662
            }
110✔
663
        }
664
        None
132✔
665
    }
132✔
666

667
    /// If `file_name` is not None, load the file. Then run the text editor.
668
    ///
669
    /// # Errors
670
    ///
671
    /// Will Return `Err` if any error occur.
UNCOV
672
    pub fn run<I: BufRead>(&mut self, file_name: Option<&str>, input: &mut I) -> Result<(), Error> {
×
UNCOV
673
        self.update_window_size()?;
×
UNCOV
674
        set_status!(self, "{HELP_MESSAGE}");
×
UNCOV
675
        self.refresh_screen()?;
×
676

UNCOV
677
        if let Some(path) = file_name.map(sys::path) {
×
UNCOV
678
            self.syntax = SyntaxConf::find(&path.to_string_lossy(), &sys::data_dirs());
×
UNCOV
679
            self.load(path.as_path())?;
×
UNCOV
680
            self.file_name = Some(path.to_string_lossy().to_string());
×
UNCOV
681
        } else {
×
UNCOV
682
            self.rows.push(Row::new(Vec::new()));
×
UNCOV
683
            self.file_name = None;
×
UNCOV
684
        }
×
685
        loop {
686
            if let Some(mode) = &self.prompt_mode {
×
687
                set_status!(self, "{}", mode.status_msg());
×
688
            }
×
689
            self.refresh_screen()?;
×
UNCOV
690
            let key = self.loop_until_keypress(input)?;
×
691
            // TODO: Can we avoid using take()?
UNCOV
692
            self.prompt_mode = match self.prompt_mode.take() {
×
693
                // process_keypress returns (should_quit, prompt_mode)
UNCOV
694
                None => match self.process_keypress(&key) {
×
UNCOV
695
                    (true, _) => return Ok(()),
×
UNCOV
696
                    (false, prompt_mode) => prompt_mode,
×
697
                },
UNCOV
698
                Some(prompt_mode) => prompt_mode.process_keypress(self, &key),
×
699
            }
700
        }
701
    }
×
702
}
703

704
/// Set up the terminal and run the text editor. If `file_name` is not None,
705
/// load the file.
706
///
707
/// Update the panic hook to restore the terminal on panic.
708
///
709
/// # Errors
710
///
711
/// Will Return `Err` if any error occur when registering the window size signal
712
/// handler, enabling raw mode, or running the editor.
713
pub fn run<I: BufRead>(file_name: Option<&str>, input: &mut I) -> Result<(), Error> {
120✔
714
    sys::register_winsize_change_signal_handler()?;
120✔
715
    let orig_term_mode = sys::enable_raw_mode()?;
120✔
716
    print!("{USE_ALTERNATE_SCREEN}");
×
717

NEW
718
    let prev_hook = std::panic::take_hook();
×
NEW
719
    std::panic::set_hook(Box::new(move |info| {
×
NEW
720
        terminal::restore_terminal(&orig_term_mode).unwrap_or_else(|e| eprintln!("{e}"));
×
NEW
721
        prev_hook(info);
×
NEW
722
    }));
×
723

UNCOV
724
    let mut editor = Editor { config: Config::load(), ..Default::default() };
×
UNCOV
725
    let result = editor.run(file_name, input);
×
726

727
    // Restore the original terminal mode.
NEW
728
    terminal::restore_terminal(&orig_term_mode)?;
×
729

UNCOV
730
    result
×
731
}
120✔
732

733
/// The prompt mode.
734
#[cfg_attr(test, derive(Debug, PartialEq))]
735
enum PromptMode {
736
    /// Save(prompt buffer)
737
    Save(String),
738
    /// Find(prompt buffer, saved cursor state, last match)
739
    Find(String, CursorState, Option<usize>),
740
    /// GoTo(prompt buffer)
741
    GoTo(String),
742
    /// Execute(prompt buffer)
743
    Execute(String),
744
}
745

746
// TODO: Use trait with mode_status_msg and process_keypress, implement the
747
// trait for separate  structs for Save and Find?
748
impl PromptMode {
749
    /// Return the status message to print for the selected `PromptMode`.
UNCOV
750
    fn status_msg(&self) -> String {
×
UNCOV
751
        match self {
×
UNCOV
752
            Self::Save(buffer) => format!("Save as: {buffer}"),
×
UNCOV
753
            Self::Find(buffer, ..) => format!("Search (Use ESC/Arrows/Enter): {buffer}"),
×
UNCOV
754
            Self::GoTo(buffer) => format!("Enter line number[:column number]: {buffer}"),
×
UNCOV
755
            Self::Execute(buffer) => format!("Command to execute: {buffer}"),
×
756
        }
UNCOV
757
    }
×
758

759
    /// Process a keypress event for the selected `PromptMode`.
760
    fn process_keypress(self, ed: &mut Editor, key: &Key) -> Option<Self> {
154✔
761
        ed.status_msg = None;
154✔
762
        match self {
154✔
UNCOV
763
            Self::Save(b) => match process_prompt_keypress(b, key) {
×
UNCOV
764
                PromptState::Active(b) => return Some(Self::Save(b)),
×
765
                PromptState::Cancelled => set_status!(ed, "Save aborted"),
×
766
                PromptState::Completed(file_name) => ed.save_as(file_name),
×
767
            },
768
            Self::Find(b, saved_cursor, last_match) => {
154✔
769
                if let Some(row_idx) = last_match {
154✔
770
                    ed.rows[row_idx].match_segment = None;
×
771
                }
154✔
772
                match process_prompt_keypress(b, key) {
154✔
773
                    PromptState::Active(query) => {
132✔
774
                        #[expect(clippy::wildcard_enum_match_arm)]
775
                        let (last_match, forward) = match key {
132✔
776
                            Key::Arrow(AKey::Right | AKey::Down) | Key::Char(FIND) =>
UNCOV
777
                                (last_match, true),
×
778
                            Key::Arrow(AKey::Left | AKey::Up) => (last_match, false),
×
779
                            _ => (None, true),
132✔
780
                        };
781
                        let curr_match = ed.find(&query, last_match, forward);
132✔
782
                        return Some(Self::Find(query, saved_cursor, curr_match));
132✔
783
                    }
784
                    // The prompt was cancelled. Restore the previous position.
785
                    PromptState::Cancelled => ed.cursor = saved_cursor,
×
786
                    // Cursor has already been moved, do nothing
787
                    PromptState::Completed(_) => (),
22✔
788
                }
789
            }
UNCOV
790
            Self::GoTo(b) => match process_prompt_keypress(b, key) {
×
UNCOV
791
                PromptState::Active(b) => return Some(Self::GoTo(b)),
×
792
                PromptState::Cancelled => (),
×
793
                PromptState::Completed(b) => {
×
UNCOV
794
                    let mut split = b.splitn(2, ':')
×
795
                        // saturating_sub: Lines and cols are 1-indexed
UNCOV
796
                        .map(|u| u.trim().parse().map(|s: usize| s.saturating_sub(1)));
×
UNCOV
797
                    match (split.next().transpose(), split.next().transpose()) {
×
UNCOV
798
                        (Ok(Some(y)), Ok(x)) => {
×
UNCOV
799
                            ed.cursor.y = y.min(ed.rows.len());
×
800
                            if let Some(rx) = x {
×
UNCOV
801
                                ed.cursor.x = ed.current_row().map_or(0, |r| r.rx2cx[rx]);
×
UNCOV
802
                            } else {
×
UNCOV
803
                                ed.update_cursor_x_position();
×
UNCOV
804
                            }
×
805
                        }
806
                        (Err(e), _) | (_, Err(e)) => set_status!(ed, "Parsing error: {e}"),
×
807
                        (Ok(None), _) => (),
×
808
                    }
809
                }
810
            },
811
            Self::Execute(b) => match process_prompt_keypress(b, key) {
×
812
                PromptState::Active(b) => return Some(Self::Execute(b)),
×
813
                PromptState::Cancelled => (),
×
814
                PromptState::Completed(b) => {
×
815
                    let mut args = b.split_whitespace();
×
816
                    match Command::new(args.next().unwrap_or_default()).args(args).output() {
×
817
                        Ok(out) if !out.status.success() =>
×
818
                            set_status!(ed, "{}", String::from_utf8_lossy(&out.stderr).trim_end()),
×
819
                        Ok(out) => out.stdout.into_iter().for_each(|c| match c {
×
UNCOV
820
                            b'\n' => ed.insert_new_line(),
×
821
                            c => ed.insert_byte(c),
×
822
                        }),
×
UNCOV
823
                        Err(e) => set_status!(ed, "{e}"),
×
824
                    }
825
                }
826
            },
827
        }
828
        None
22✔
829
    }
154✔
830
}
831

832
/// The state of the prompt after processing a keypress event.
833
#[cfg_attr(test, derive(Debug, PartialEq))]
834
enum PromptState {
835
    // Active contains the current buffer
836
    Active(String),
837
    // Completed contains the final string
838
    Completed(String),
839
    Cancelled,
840
}
841

842
/// Process a prompt keypress event and return the new state for the prompt.
843
fn process_prompt_keypress(mut buffer: String, key: &Key) -> PromptState {
374✔
844
    #[expect(clippy::wildcard_enum_match_arm)]
845
    match key {
209✔
846
        Key::Char(b'\r') => return PromptState::Completed(buffer),
33✔
847
        Key::Escape | Key::Char(EXIT) => return PromptState::Cancelled,
22✔
848
        Key::Char(BACKSPACE | DELETE_BIS) => _ = buffer.pop(),
99✔
849
        Key::Char(c @ 0..=126) if !c.is_ascii_control() => buffer.push(*c as char),
220✔
850
        // No-op
851
        _ => (),
22✔
852
    }
853
    PromptState::Active(buffer)
319✔
854
}
374✔
855

856
#[cfg(test)]
857
mod tests {
858
    use std::io::Cursor;
859

860
    use rstest::rstest;
861

862
    use super::*;
863

864
    fn assert_row_chars_equal(editor: &Editor, expected: &[&[u8]]) {
55✔
865
        assert_eq!(editor.rows.len(), expected.len());
55✔
866
        for (i, (row, expected)) in editor.rows.iter().zip(expected).enumerate() {
77✔
867
            assert_eq!(
77✔
868
                row.chars,
869
                *expected,
UNCOV
870
                "comparing characters for row {}\n  left: {}\n  right: {})",
×
871
                i,
UNCOV
872
                String::from_utf8_lossy(&row.chars),
×
UNCOV
873
                String::from_utf8_lossy(expected)
×
874
            );
875
        }
876
    }
55✔
877

878
    #[rstest]
879
    #[case(0, "0B")]
880
    #[case(1, "1B")]
881
    #[case(1023, "1023B")]
882
    #[case(1024, "1.00kB")]
883
    #[case(1536, "1.50kB")]
884
    // round down!
885
    #[case(21 * 1024 - 11, "20.98kB")]
886
    #[case(21 * 1024 - 10, "20.99kB")]
887
    #[case(21 * 1024 - 3, "20.99kB")]
888
    #[case(21 * 1024, "21.00kB")]
889
    #[case(21 * 1024 + 3, "21.00kB")]
890
    #[case(21 * 1024 + 10, "21.00kB")]
891
    #[case(21 * 1024 + 11, "21.01kB")]
892
    #[case(1024 * 1024 - 1, "1023.99kB")]
893
    #[case(1024 * 1024, "1.00MB")]
894
    #[case(1024 * 1024 + 1, "1.00MB")]
895
    #[case(100 * 1024 * 1024 * 1024, "100.00GB")]
896
    #[case(313 * 1024 * 1024 * 1024 * 1024, "313.00TB")]
897
    fn format_size_output(#[case] input: u64, #[case] expected_output: &str) {
898
        assert_eq!(format_size(input), expected_output);
899
    }
900

901
    #[test]
902
    fn editor_insert_byte() {
11✔
903
        let mut editor = Editor::default();
11✔
904
        let editor_cursor_x_before = editor.cursor.x;
11✔
905

906
        editor.insert_byte(b'X');
11✔
907
        editor.insert_byte(b'Y');
11✔
908
        editor.insert_byte(b'Z');
11✔
909

910
        assert_eq!(editor.cursor.x, editor_cursor_x_before + 3);
11✔
911
        assert_eq!(editor.rows.len(), 1);
11✔
912
        assert_eq!(editor.n_bytes, 3);
11✔
913
        assert_eq!(editor.rows[0].chars, [b'X', b'Y', b'Z']);
11✔
914
    }
11✔
915

916
    #[test]
917
    fn editor_insert_new_line() {
11✔
918
        let mut editor = Editor::default();
11✔
919
        let editor_cursor_y_before = editor.cursor.y;
11✔
920

921
        for _ in 0..3 {
44✔
922
            editor.insert_new_line();
33✔
923
        }
33✔
924

925
        assert_eq!(editor.cursor.y, editor_cursor_y_before + 3);
11✔
926
        assert_eq!(editor.rows.len(), 3);
11✔
927
        assert_eq!(editor.n_bytes, 0);
11✔
928

929
        for row in &editor.rows {
44✔
930
            assert_eq!(row.chars, []);
33✔
931
        }
932
    }
11✔
933

934
    #[test]
935
    fn editor_delete_char() {
11✔
936
        let mut editor = Editor::default();
11✔
937
        for b in b"Hello world!" {
143✔
938
            editor.insert_byte(*b);
132✔
939
        }
132✔
940
        editor.delete_char();
11✔
941
        assert_row_chars_equal(&editor, &[b"Hello world"]);
11✔
942
        editor.move_cursor(&AKey::Left, true);
11✔
943
        editor.move_cursor(&AKey::Left, false);
11✔
944
        editor.move_cursor(&AKey::Left, false);
11✔
945
        editor.delete_char();
11✔
946
        assert_row_chars_equal(&editor, &[b"Helo world"]);
11✔
947
    }
11✔
948

949
    #[test]
950
    fn editor_delete_next_char() {
11✔
951
        let mut editor = Editor::default();
11✔
952
        for &b in b"Hello world!\nHappy New Year!" {
319✔
953
            editor.process_keypress(&Key::Char(b));
308✔
954
        }
308✔
955
        editor.process_keypress(&Key::Delete);
11✔
956
        assert_row_chars_equal(&editor, &[b"Hello world!", b"Happy New Year!"]);
11✔
957
        editor.move_cursor(&AKey::Left, true);
11✔
958
        editor.process_keypress(&Key::Delete);
11✔
959
        assert_row_chars_equal(&editor, &[b"Hello world!", b"Happy New ear!"]);
11✔
960
        editor.move_cursor(&AKey::Left, true);
11✔
961
        editor.move_cursor(&AKey::Left, true);
11✔
962
        editor.move_cursor(&AKey::Left, true);
11✔
963
        editor.process_keypress(&Key::Delete);
11✔
964
        assert_row_chars_equal(&editor, &[b"Hello world!Happy New ear!"]);
11✔
965
    }
11✔
966

967
    #[test]
968
    fn editor_move_cursor_left() {
11✔
969
        let mut editor = Editor::default();
11✔
970
        for &b in b"Hello world!\nHappy New Year!" {
319✔
971
            editor.process_keypress(&Key::Char(b));
308✔
972
        }
308✔
973

974
        // check current position
975
        assert_eq!(editor.cursor.x, 15);
11✔
976
        assert_eq!(editor.cursor.y, 1);
11✔
977

978
        editor.move_cursor(&AKey::Left, true);
11✔
979
        assert_eq!(editor.cursor.x, 10);
11✔
980
        assert_eq!(editor.cursor.y, 1);
11✔
981

982
        editor.move_cursor(&AKey::Left, false);
11✔
983
        assert_eq!(editor.cursor.x, 9);
11✔
984
        assert_eq!(editor.cursor.y, 1);
11✔
985

986
        editor.move_cursor(&AKey::Left, true);
11✔
987
        assert_eq!(editor.cursor.x, 6);
11✔
988
        assert_eq!(editor.cursor.y, 1);
11✔
989

990
        editor.move_cursor(&AKey::Left, true);
11✔
991
        assert_eq!(editor.cursor.x, 0);
11✔
992
        assert_eq!(editor.cursor.y, 1);
11✔
993

994
        editor.move_cursor(&AKey::Left, false);
11✔
995
        assert_eq!(editor.cursor.x, 12);
11✔
996
        assert_eq!(editor.cursor.y, 0);
11✔
997

998
        editor.move_cursor(&AKey::Left, true);
11✔
999
        assert_eq!(editor.cursor.x, 6);
11✔
1000
        assert_eq!(editor.cursor.y, 0);
11✔
1001

1002
        editor.move_cursor(&AKey::Left, true);
11✔
1003
        assert_eq!(editor.cursor.x, 0);
11✔
1004
        assert_eq!(editor.cursor.y, 0);
11✔
1005

1006
        editor.move_cursor(&AKey::Left, false);
11✔
1007
        assert_eq!(editor.cursor.x, 0);
11✔
1008
        assert_eq!(editor.cursor.y, 0);
11✔
1009
    }
11✔
1010

1011
    #[test]
1012
    fn editor_move_cursor_up() {
11✔
1013
        let mut editor = Editor::default();
11✔
1014
        for &b in b"abcdefgh\nij\nklmnopqrstuvwxyz" {
319✔
1015
            editor.process_keypress(&Key::Char(b));
308✔
1016
        }
308✔
1017

1018
        // check current position
1019
        assert_eq!(editor.cursor.x, 16);
11✔
1020
        assert_eq!(editor.cursor.y, 2);
11✔
1021

1022
        editor.move_cursor(&AKey::Up, false);
11✔
1023
        assert_eq!(editor.cursor.x, 2);
11✔
1024
        assert_eq!(editor.cursor.y, 1);
11✔
1025

1026
        editor.move_cursor(&AKey::Up, true);
11✔
1027
        assert_eq!(editor.cursor.x, 2);
11✔
1028
        assert_eq!(editor.cursor.y, 0);
11✔
1029

1030
        editor.move_cursor(&AKey::Up, false);
11✔
1031
        assert_eq!(editor.cursor.x, 2);
11✔
1032
        assert_eq!(editor.cursor.y, 0);
11✔
1033
    }
11✔
1034

1035
    #[test]
1036
    fn editor_move_cursor_right() {
11✔
1037
        let mut editor = Editor::default();
11✔
1038
        for &b in b"Hello world\nHappy New Year" {
297✔
1039
            editor.process_keypress(&Key::Char(b));
286✔
1040
        }
286✔
1041

1042
        // check current position
1043
        assert_eq!(editor.cursor.x, 14);
11✔
1044
        assert_eq!(editor.cursor.y, 1);
11✔
1045

1046
        editor.move_cursor(&AKey::Right, false);
11✔
1047
        assert_eq!(editor.cursor.x, 0);
11✔
1048
        assert_eq!(editor.cursor.y, 2);
11✔
1049

1050
        editor.move_cursor(&AKey::Right, false);
11✔
1051
        assert_eq!(editor.cursor.x, 0);
11✔
1052
        assert_eq!(editor.cursor.y, 2);
11✔
1053

1054
        editor.move_cursor(&AKey::Up, true);
11✔
1055
        editor.move_cursor(&AKey::Up, true);
11✔
1056
        assert_eq!(editor.cursor.x, 0);
11✔
1057
        assert_eq!(editor.cursor.y, 0);
11✔
1058

1059
        editor.move_cursor(&AKey::Right, true);
11✔
1060
        assert_eq!(editor.cursor.x, 5);
11✔
1061
        assert_eq!(editor.cursor.y, 0);
11✔
1062

1063
        editor.move_cursor(&AKey::Right, true);
11✔
1064
        assert_eq!(editor.cursor.x, 11);
11✔
1065
        assert_eq!(editor.cursor.y, 0);
11✔
1066

1067
        editor.move_cursor(&AKey::Right, false);
11✔
1068
        assert_eq!(editor.cursor.x, 0);
11✔
1069
        assert_eq!(editor.cursor.y, 1);
11✔
1070
    }
11✔
1071

1072
    #[test]
1073
    fn editor_move_cursor_down() {
11✔
1074
        let mut editor = Editor::default();
11✔
1075
        for &b in b"abcdefgh\nij\nklmnopqrstuvwxyz" {
319✔
1076
            editor.process_keypress(&Key::Char(b));
308✔
1077
        }
308✔
1078

1079
        // check current position
1080
        assert_eq!(editor.cursor.x, 16);
11✔
1081
        assert_eq!(editor.cursor.y, 2);
11✔
1082

1083
        editor.move_cursor(&AKey::Down, false);
11✔
1084
        assert_eq!(editor.cursor.x, 0);
11✔
1085
        assert_eq!(editor.cursor.y, 3);
11✔
1086

1087
        editor.move_cursor(&AKey::Up, false);
11✔
1088
        editor.move_cursor(&AKey::Up, false);
11✔
1089
        editor.move_cursor(&AKey::Up, false);
11✔
1090

1091
        assert_eq!(editor.cursor.x, 0);
11✔
1092
        assert_eq!(editor.cursor.y, 0);
11✔
1093

1094
        editor.move_cursor(&AKey::Right, true);
11✔
1095
        assert_eq!(editor.cursor.x, 8);
11✔
1096
        assert_eq!(editor.cursor.y, 0);
11✔
1097

1098
        editor.move_cursor(&AKey::Down, true);
11✔
1099
        assert_eq!(editor.cursor.x, 2);
11✔
1100
        assert_eq!(editor.cursor.y, 1);
11✔
1101

1102
        editor.move_cursor(&AKey::Down, true);
11✔
1103
        assert_eq!(editor.cursor.x, 2);
11✔
1104
        assert_eq!(editor.cursor.y, 2);
11✔
1105

1106
        editor.move_cursor(&AKey::Down, true);
11✔
1107
        assert_eq!(editor.cursor.x, 0);
11✔
1108
        assert_eq!(editor.cursor.y, 3);
11✔
1109

1110
        editor.move_cursor(&AKey::Down, false);
11✔
1111
        assert_eq!(editor.cursor.x, 0);
11✔
1112
        assert_eq!(editor.cursor.y, 3);
11✔
1113
    }
11✔
1114

1115
    #[test]
1116
    fn editor_press_home_key() {
11✔
1117
        let mut editor = Editor::default();
11✔
1118
        for &b in b"Hello\nWorld\nand\nFerris!" {
264✔
1119
            editor.process_keypress(&Key::Char(b));
253✔
1120
        }
253✔
1121

1122
        // check current position
1123
        assert_eq!(editor.cursor.x, 7);
11✔
1124
        assert_eq!(editor.cursor.y, 3);
11✔
1125

1126
        editor.process_keypress(&Key::Home);
11✔
1127
        assert_eq!(editor.cursor.x, 0);
11✔
1128
        assert_eq!(editor.cursor.y, 3);
11✔
1129

1130
        editor.move_cursor(&AKey::Up, false);
11✔
1131
        editor.move_cursor(&AKey::Up, false);
11✔
1132
        editor.move_cursor(&AKey::Up, false);
11✔
1133

1134
        assert_eq!(editor.cursor.x, 0);
11✔
1135
        assert_eq!(editor.cursor.y, 0);
11✔
1136

1137
        editor.move_cursor(&AKey::Right, true);
11✔
1138
        assert_eq!(editor.cursor.x, 5);
11✔
1139
        assert_eq!(editor.cursor.y, 0);
11✔
1140

1141
        editor.process_keypress(&Key::Home);
11✔
1142
        assert_eq!(editor.cursor.x, 0);
11✔
1143
        assert_eq!(editor.cursor.y, 0);
11✔
1144
    }
11✔
1145

1146
    #[test]
1147
    fn editor_press_end_key() {
11✔
1148
        let mut editor = Editor::default();
11✔
1149
        for &b in b"Hello\nWorld\nand\nFerris!" {
264✔
1150
            editor.process_keypress(&Key::Char(b));
253✔
1151
        }
253✔
1152

1153
        // check current position
1154
        assert_eq!(editor.cursor.x, 7);
11✔
1155
        assert_eq!(editor.cursor.y, 3);
11✔
1156

1157
        editor.process_keypress(&Key::End);
11✔
1158
        assert_eq!(editor.cursor.x, 7);
11✔
1159
        assert_eq!(editor.cursor.y, 3);
11✔
1160

1161
        editor.move_cursor(&AKey::Up, false);
11✔
1162
        editor.move_cursor(&AKey::Up, false);
11✔
1163
        editor.move_cursor(&AKey::Up, false);
11✔
1164

1165
        assert_eq!(editor.cursor.x, 3);
11✔
1166
        assert_eq!(editor.cursor.y, 0);
11✔
1167

1168
        editor.process_keypress(&Key::End);
11✔
1169
        assert_eq!(editor.cursor.x, 5);
11✔
1170
        assert_eq!(editor.cursor.y, 0);
11✔
1171
    }
11✔
1172

1173
    #[test]
1174
    fn loop_until_keypress() -> Result<(), Error> {
11✔
1175
        let mut editor = Editor::default();
11✔
1176
        let mut fake_stdin = Cursor::new(
11✔
1177
            b"abc\x1b[A\x1b[B\x1b[C\x1b[D\x1b[H\x1bOH\x1b[F\x1bOF\x1b[1;5C\x1b[5C\x1b[99",
1178
        );
1179
        for expected_key in [
154✔
1180
            Key::Char(b'a'),
11✔
1181
            Key::Char(b'b'),
11✔
1182
            Key::Char(b'c'),
11✔
1183
            Key::Arrow(AKey::Up),
11✔
1184
            Key::Arrow(AKey::Down),
11✔
1185
            Key::Arrow(AKey::Right),
11✔
1186
            Key::Arrow(AKey::Left),
11✔
1187
            Key::Home,
11✔
1188
            Key::Home,
11✔
1189
            Key::End,
11✔
1190
            Key::End,
11✔
1191
            Key::CtrlArrow(AKey::Right),
11✔
1192
            Key::CtrlArrow(AKey::Right),
11✔
1193
            Key::Escape,
11✔
1194
        ] {
1195
            assert_eq!(editor.loop_until_keypress(&mut fake_stdin)?, expected_key);
154✔
1196
        }
1197
        Ok(())
11✔
1198
    }
11✔
1199

1200
    #[rstest]
1201
    #[case::ascii_completed(&[Key::Char(b'H'), Key::Char(b'i'), Key::Char(b'\r')], &PromptState::Completed(String::from("Hi")))]
1202
    #[case::escape(&[Key::Char(b'H'), Key::Char(b'i'), Key::Escape], &PromptState::Cancelled)]
1203
    #[case::exit(&[Key::Char(b'H'), Key::Char(b'i'), Key::Char(EXIT)], &PromptState::Cancelled)]
1204
    #[case::skip_ascii_control(&[Key::Char(b'\x0A')], &PromptState::Active(String::new()))]
1205
    #[case::unsupported_non_ascii(&[Key::Char(b'\xEF')], &PromptState::Active(String::new()))]
1206
    #[case::backspace(&[Key::Char(b'H'), Key::Char(b'i'), Key::Char(BACKSPACE), Key::Char(BACKSPACE)], &PromptState::Active(String::new()))]
1207
    #[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()))]
1208
    fn process_prompt_keypresses(#[case] keys: &[Key], #[case] expected_final_state: &PromptState) {
1209
        let mut prompt_state = PromptState::Active(String::new());
1210
        for key in keys {
1211
            if let PromptState::Active(buffer) = prompt_state {
1212
                prompt_state = process_prompt_keypress(buffer, key);
1213
            } else {
1214
                panic!("Prompt state: {prompt_state:?} is not active")
1215
            }
1216
        }
1217
        assert_eq!(prompt_state, *expected_final_state);
1218
    }
1219

1220
    #[rstest]
1221
    #[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")]
1222
    #[case(&[Key::Char(b'H'), Key::Char(b'i'), Key::Char(BACKSPACE), Key::Char(BACKSPACE), Key::Char(BACKSPACE)], "")]
1223
    fn process_find_keypress_completed(#[case] keys: &[Key], #[case] expected_final_value: &str) {
1224
        let mut ed: Editor = Editor::default();
1225
        ed.insert_new_line();
1226
        let mut prompt_mode = Some(PromptMode::Find(String::new(), CursorState::default(), None));
1227
        for key in keys {
1228
            prompt_mode = prompt_mode
1229
                .take()
1230
                .and_then(|prompt_mode| prompt_mode.process_keypress(&mut ed, key));
132✔
1231
        }
1232
        assert_eq!(
1233
            prompt_mode,
1234
            Some(PromptMode::Find(
1235
                String::from(expected_final_value),
1236
                CursorState::default(),
1237
                None
1238
            ))
1239
        );
1240
        prompt_mode = prompt_mode
1241
            .take()
1242
            .and_then(|prompt_mode| prompt_mode.process_keypress(&mut ed, &Key::Char(b'\r')));
22✔
1243
        assert_eq!(prompt_mode, None);
1244
    }
1245
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc